第 6 章 · 模块与包¶
Java 用 package + classpath + 一个文件一个 public class 组织代码。Python 的组织方式完全不同:一个 .py 文件就是一个模块,一个含 __init__.py 的目录就是一个包,靠 import 和 sys.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
-
Python
四种写法:
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.py、random.py、csv.py等标准库名。当前目录在sys.path最前,会遮蔽标准库,导致诡异的ImportError。这是新手最常踩的坑之一。 - 不要靠"把当前目录加进 classpath"那种全局环境变量管依赖。用虚拟环境(第 10 章)隔离每个项目的依赖。
6.6 循环导入¶
Java 里类间相互引用很平常。Python 里模块级循环导入会出问题:
如果加载 a 时它还没定义完 func_a,b 就拿不到——报 ImportError 或 AttributeError。
解决思路:
- 重构:把共享的东西抽到第三个模块,打破环。
- 延迟导入:把
import移进函数体内,用到时才导入。 - 只导入模块、不导入名字:
import a然后a.func_a(),因为模块对象在 import 时就已存在。
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 让 app 和 app.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 惯用法。