跳转至

第 6 章 · 模块与包

Java 用 package + classpath + 一个文件一个 public class 组织代码。Python 的组织方式完全不同:一个 .py 文件就是一个模块一个含 __init__.py 的目录就是一个包,靠 importsys.path 找东西。理解这一章,你才能读懂任何 Python 项目的结构。


6.1 基本概念

概念 Python Java
一个源文件 模块utils.py → 模块 utils 一个 .java(一个 public class
一个目录 (含 __init__.py package(目录映射包名)
复用单元 模块 / 包 类 / jar
查找路径 sys.path classpath

一个模块文件里可以放多个类、函数、变量——不像 Java 一个文件只能有一个 public 类。


6.2 import 的四种姿势

  • Java

    import java.util.List;
    import java.util.*;          // 通配
    import static java.lang.Math.PI;
    
  • Python

    import utils                 # 导入模块,用 utils.helper()
    from utils import helper     # 导入名字,直接 helper()
    from utils import *          # 不推荐(污染命名空间)
    import numpy as np           # 别名(极常用)
    

四种写法:

import json                          # 整个模块,json.loads(...)
from collections import defaultdict  # 只拿名字
from package.module import ClassA, func_b   # 从包里深入
import pandas as pd                  # 别名(社区约定:np/pd/plt)

⚠️ Java 程序员的陷阱

  • from x import * 不要用(除非交互式探索)。它把模块所有名字倒进当前作用域,掩盖同名、难追踪来源。Java 的通配 import 只影响编译期名字解析,Python 的 * 是运行时污染,性质不同。
  • Python 的 import语句,可以出现在函数内部(局部导入,延迟加载),而 Java 的 import 只能在文件顶部。

6.3 if __name__ == "__main__":Python 的"main 方法"

Java 的入口是 public static void main(String[] args)。Python 没有专门的入口语法——约定用这个判断:

# greet.py
def greet(name: str) -> str:
    return f"Hi, {name}!"

# 这块代码只在"直接运行"本文件时执行;
# 当 greet.py 被别的模块 import 时,不执行。
if __name__ == "__main__":
    print(greet("World"))

原理:每个模块都有个特殊变量 __name__。直接运行时它是 "__main__";被导入时它是模块名(如 "greet")。

Pythonic 写法

这是 Python 写"既能被导入复用、又能独立运行"脚本的标准模式。把可复用的函数/类放外面,把"当脚本跑时的副作用"(打印、命令行入口)放进 if __name__ == "__main__":

命令行参数用 sys.argv(或更好的 argparse,贯穿项目会用):

import sys
if __name__ == "__main__":
    name = sys.argv[1] if len(sys.argv) > 1 else "World"
    print(greet(name))

6.4 包结构

一个典型 Python 项目:

myapp/
├── __init__.py        # 标记 myapp 是个包(可为空)
├── models/
│   ├── __init__.py
│   └── user.py        # 模块 myapp.models.user
├── services.py        # 模块 myapp.services
└── cli.py
  • __init__.py 标记一个目录是,并在"包被导入时"执行一次(常用来做包级初始化、聚合导出)。
  • 导入 myapp.models.user 里的 User 类:from myapp.models.user import User
  • Python 3.3+ 命名空间包:目录没有 __init__.py 也能当包(用于跨目录合并包),但普通项目仍建议保留 __init__.py,语义清晰。

6.5 模块搜索路径:sys.path

Python 按 sys.path 里的顺序找模块(类似 classpath):

import sys
print(sys.path)
# 1. 当前脚本所在目录(最优先)
# 2. 环境变量 PYTHONPATH 里的目录
# 3. 解释器自带的 + 第三方 site-packages

第三方包(你用 uv / pip 装的)在 site-packages 里——相当于 Maven 的本地仓库 ~/.m2

⚠️ Java 程序员的陷阱

  • 不要把自己的文件命名为 math.pyrandom.pycsv.py 等标准库名。当前目录在 sys.path 最前,会遮蔽标准库,导致诡异的 ImportError。这是新手最常踩的坑之一。
  • 不要靠"把当前目录加进 classpath"那种全局环境变量管依赖。用虚拟环境(第 10 章)隔离每个项目的依赖。

6.6 循环导入

Java 里类间相互引用很平常。Python 里模块级循环导入会出问题:

# a.py
from b import func_b      # a 导入 b

# b.py
from a import func_a      # b 又导入 a —— 循环!

如果加载 a 时它还没定义完 func_ab 就拿不到——报 ImportErrorAttributeError

解决思路

  1. 重构:把共享的东西抽到第三个模块,打破环。
  2. 延迟导入:把 import 移进函数体内,用到时才导入。
  3. 只导入模块、不导入名字import a 然后 a.func_a(),因为模块对象在 import 时就已存在。
# b.py —— 用函数内导入打破循环
def use_a():
    from a import func_a     # 运行时才导入
    func_a()

6.7 __all__:控制 from x import * 的范围

# mymodule.py
__all__ = ["public_func", "PublicClass"]   # 只导出这些

def public_func(): ...
def _private(): ...            # 不在 __all__,不被 * 导出
class PublicClass: ...

即便你不用 *__all__ 也用来声明模块的公开 API(类似 Java 的访问控制补充)。


本章练习

练习 6.1

创建这样的项目结构,并写出正确的 import 语句在 main.py 中使用 User 类和 greet 函数:

app/
  __init__.py
  models/__init__.py
  models/user.py      # class User
  utils.py            # def greet(name)
main.py

参考答案

# main.py
from app.models.user import User
from app.utils import greet
要点:从包根 app 开始的绝对导入;__init__.pyappapp.models 成为包。

练习 6.2

你写了个脚本叫 random.py 来练习随机数,结果 import random; random.randint(...) 报诡异的 AttributeError。为什么?怎么修?

参考答案

你的 random.py 遮蔽了标准库 random——当前目录在 sys.path 最前,import random 找到的是你自己的文件,而它没有 randint。修复:把自己的文件改名(如 my_random.py),并删除误生成的 __pycache__/.pyc

练习 6.3

说明 if __name__ == "__main__": 的作用,并解释为什么把"打印调试信息"直接写在模块顶层(不在该块里)是个坏习惯。

参考答案

__name__ 在直接运行时为 "__main__"、被导入时为模块名。该块保证副作用只在直接运行时发生。把 print 放顶层会导致任何 import 该模块的代码都触发打印,污染使用者、可能拖慢导入、还可能在导入环境(如测试、Web 服务)里产生意外输出。


上一章:第 5 章 · 异常 | 下一章:第 7 章 · Pythonic 惯用法