本篇整理 Python 模块与包的组织方式。
模块解决单个文件的命名空间问题,包解决多个模块的目录组织问题。
1. 模块
1.1 模块对象
一个普通 .py 文件就是一个 module。导入模块后,当前命名空间会得到一个指向模块对象的名字。
import sysprint(type(sys).__name__) # moduleprint(isinstance(sys.argv, list)) # Trueimport sys 之后,sys 是模块对象。访问 sys.argv、sys.path 这类成员时,实际是在访问 sys 模块命名空间里的变量。
1.2 模块成员
模块成员就是定义在模块命名空间里的名字。函数、类、常量、导入进来的对象和运行时特殊变量都可以成为模块成员。
"""Greeting module."""__author__ = 'qianshuang'DEFAULT_NAME = 'world'def greeting(name: str = DEFAULT_NAME) -> str: return f'Hello, {name}!'print(__doc__) # Greeting module.print(__author__) # qianshuangprint(greeting('Ada')) # Hello, Ada!模块顶层的第一个字符串字面量会成为模块文档字符串,即 __doc__。__author__、__version__ 这类名字只是普通变量,但常用于记录模块元数据。
__name__ 是模块使用方式的标记。模块被导入时,__name__ 通常是模块导入名;文件被直接作为顶层脚本运行时,__name__ 是
'__main__'。
print(__name__) # __main__常见特殊成员如下:
| 成员 | 含义 |
|---|---|
__name__ | 当前模块名;直接运行时为 '__main__' |
__doc__ | 模块文档字符串 |
__file__ | 模块文件路径 |
__spec__ | 模块导入规格,包含来源、加载器等信息 |
双下划线包围的名字通常是 Python 运行时约定的特殊名字。业务代码不应该随意发明新的 __xxx__ 名字,避免和语言或工具约定冲突。
1.3 顶层代码
模块顶层代码是文件中不在函数体、类体内部的代码。不只是赋值语句,位于顶层的函数定义、类定义、赋值语句和 import 等都属于顶层代码。
import mathVALUE = math.sqrt(16)def get_value() -> float: return VALUEprint(get_value()) # 4.0顶层代码在模块第一次被导入,或文件作为顶层脚本运行时执行。一般用于配置模块元数据、定义常量和初始化。
1.4 导入时机
import 是普通语句,可以出现在模块顶层、函数体、if 分支、循环体等位置。代码执行到该语句时,才会触发导入流程。
def calculate(enabled: bool) -> float: if enabled: import math return math.sqrt(16) return 0.0print(calculate(False)) # 0.0print(calculate(True)) # 4.0calculate(False) 没有执行到 import math,因此这个调用不会触发导入;calculate(True) 执行到分支内部时才会导入 math。
1.5 使用方式
同一个 .py 模块有两种常见使用方式:被其他模块导入,或直接作为顶层模块运行。if __name__ == '__main__': 用来区分这两种路径。
# hello.pyimport sysdef greeting(argv: list[str]) -> str: if len(argv) == 1: return 'Hello, world!' if len(argv) == 2: return f'Hello, {argv[1]}!' return 'Too many arguments!'def main(argv: list[str]) -> None: print(greeting(argv))def init() -> None: print('init') # initinit()if __name__ == '__main__': def test() -> None: assert greeting(['hello.py']) == 'Hello, world!' assert greeting(['hello.py', 'Michael']) == 'Hello, Michael!' test() main(sys.argv)# python hello.py -> 先输出 init,再输出 Hello, world!# python hello.py Michael -> 先输出 init,再输出 Hello, Michael!导入 hello 时,顶层的 import sys、函数定义和 init() 会执行;if __name__ == '__main__': 分支不会执行。直接运行
hello.py 时,顶层代码和入口分支都会执行。
2. 包
2.1 目录结构
package 是包含多个模块的目录。对于一个python项目,里面的每一个文件夹都可以认为是一个package。常规 package 里会放一个
__init__.py 作为包入口,目录结构定义了包和模块的导入名。
例如,对于以下包,acme/config.py 对应 acme.config,acme/utils/text.py 对应 acme.utils.text:
(pythonrelearn) qianshuang@qianshuangdeMacBook-Air PythonRelearn % tree acme/ acme/├── __init__.py├── config.py├── service.py└── utils └── text.py2.2 入口文件
首次导入包时,__init__.py 会执行。一般用于配置包元数据与导出成员,初始化逻辑放在下属模块中。
__init__.py 本身也是模块文件,所以它也可以定义变量、函数、导入语句和特殊成员。常见成员如下:
| 成员 | 作用 |
|---|---|
__all__ | 控制 from package import * 暴露哪些名字 |
__version__ | 记录包版本 |
__author__ | 记录作者或维护者 |
| 包级导入 | 把子模块里的常用对象提升到 package 命名空间 |
下面的示例演示 __all__ 对星号导入的影响。
$ tree acmeacme└── __init__.py# acme/__init__.py__all__ = ['public_name']public_name = 'visible'_private_name = 'hidden'# main.pyfrom acme import *print(public_name) # visibleprint('_private_name' in globals()) # False__all__ 并非权限控制。调用方仍然可以写 import acme 后访问 acme._private_name;单下划线只是“不应依赖”的约定。
2.3 包级导出
包级导出把子模块中的常用对象绑定到 package 命名空间,让调用方可以从 package 直接导入。
$ tree acmeacme├── __init__.py└── config.py# acme/config.pyclass Settings: debug = True# acme/__init__.pyfrom .config import Settings__all__ = ['Settings']# main.pyimport acme print(acme.Settings.debug) # Trueprint(acme.config.Settings.debug) # True这种写法会将子模块对象绑定到 package 命名空间,避免引用时写复杂的前缀。
2.4 导入缓存
包也是模块对象,也会进入 sys.modules。首次导入 package 时,Python 会执行 package 的 __init__.py,然后把包对象缓存到
sys.modules['package']。再次导入同一个 package 时,通常直接复用这个包对象,不会重新执行 __init__.py。
导入子模块时,Python 会先确保父 package 已加载;如果父 package 尚未加载,会先执行父 package 的 __init__.py,再执行子模块文件。父
package 和子模块会分别缓存。
$ tree acmeacme├── __init__.py└── config.py# acme/__init__.pyprint('loading acme package')name = 'acme'# acme/config.pyprint('loading acme.config')debug = True# main.pyimport sysimport acme # 打印 loading acme packageimport acme # 复用缓存,无输出print(acme is sys.modules['acme']) # Trueimport acme.config # 打印 loading acme.configimport acme.config # 复用缓存,无输出print('acme.config' in sys.modules) # Trueprint(acme.config is sys.modules['acme.config']) # Trueprint(acme.config.debug) # Trueimport acme.config 和 from acme import config 都会让父 package acme 先完成加载。包缓存与模块缓存在sys.modules
中相互隔离,并且重复导入会使用缓存,不会再次执行__init__.py或模块顶层代码。
此外,由于顶层的import是模块顶层代码的一部分,因此导入时,会按顺序递归执行。
# acme/__init__.pyfrom .config import debug __all__ = ['debug']print('loading acme package')name = 'acme'# acme/config.pyprint('loading acme.config')debug = True# main.pyimport sysimport acme # 打印 loading acme.config, 打印 loading acme packageimport acme # 复用缓存,无输出在上例中,由于from .config import debug在print('loading acme package')之前,因此,loading acme.config被优先打印。
3. 导入语法
3.1 模块导入
模块导入保留命名空间。import math 后,通过 math.sqrt 访问成员,调用点能看出成员来自哪个模块。
import mathimport pathlib as pathprint(math.sqrt(16)) # 4.0print(path.Path('posts').name) # posts别名适合模块名较长或社区有稳定约定的场景。
3.2 包导入
包导入先得到包对象,再通过包对象访问已经加载的子模块或包级导出。import package 只导入包本身;它会执行包的 __init__.py
,但不会自动导入包目录下的所有子模块(除非__init__.py中包含了对子模块的引用)。
# acme/__init__.pyname = 'acme'# acme/config.pydebug = True# main.pyimport acmeprint(acme.name) # acmeprint(hasattr(acme, 'config')) # Falseimport acme.configprint(acme.config.debug) # Truefrom acme import configprint(config is acme.config) # True (复用缓存,因此是同一个对象)import acme.config 绑定的是顶层名字 acme,调用时写 acme.config.debug。from acme import config 会把 config
直接绑定到当前命名空间,调用时写 config.debug。
3.3 成员导入
成员导入把模块里的名字直接绑定到当前命名空间。from math import sqrt 之后,当前作用域里有一个 sqrt 名字,但从调用点看不出它来自哪个模块。
from math import sqrtfrom pathlib import Pathprint(sqrt(25)) # 5.0print(Path('a') / 'b') # a/b大量成员导入会让当前命名空间变宽,增加重名风险。
星号导入会把目标模块或包暴露的名字批量放入当前命名空间。普通业务代码不建议使用 from module import * 或
from package import *;它隐藏来源,也容易覆盖已有名字。
3.4 过程式导入
importlib 提供过程式导入接口,适合在运行期按字符串决定导入哪个模块。普通 import 是语句,importlib.import_module()
是函数调用,返回模块对象。
import importlibmath = importlib.import_module('math')sqrt = getattr(math, 'sqrt')print(math.sqrt(16)) # 4.0print(sqrt(25)) # 5.0常见声明式导入与过程式写法的关系如下。表中的 module 指 package 下的子模块;如果导入的是模块里的普通成员,则从模块对象上取属性。表中涉及
sys.modules 的写法需要先 import sys。
| 声明式写法 | 过程式写法 |
|---|---|
import module | module = importlib.import_module('module') |
import package | package = importlib.import_module('package') |
import package.module | importlib.import_module('package.module'); package = sys.modules['package'] |
import package.module as alias | alias = importlib.import_module('package.module') |
from package import module | module = importlib.import_module('package.module') |
from package.module import member | member = getattr(importlib.import_module('package.module'), 'member') |
from package.module import member as alias | alias = getattr(importlib.import_module('package.module'), 'member') |
importlib.reload(module) 会重新执行已经导入的模块对象,适合少量调试场景。业务代码通常不依赖 reload;长期运行的服务更常见的做法是重启进程。
4. 搜索路径
4.1 路径来源
模块查找依赖搜索路径。sys.path 是一个 list,保存解释器查找模块和包的目录顺序。
import sysprint(isinstance(sys.path, list)) # Trueprint(isinstance(sys.path[0], str)) # True解释器启动时会把几类路径放入 sys.path:
| 来源 | 说明 |
|---|---|
| 脚本目录或当前目录 | 脚本运行时通常是脚本所在目录;交互式环境里常用空字符串表示当前目录 |
PYTHONPATH | 环境变量指定的搜索路径,会在解释器启动时加入 sys.path |
| 标准库目录 | Python 自带模块所在目录 |
| 第三方包目录 | 虚拟环境或系统环境中的 site-packages |
.pth 文件追加的目录 | site 初始化时读取,常由安装工具或开发模式安装写入 |
例如:
(pythonrelearn) qianshuang@qianshuangdeMacBook-Air PythonRelearn % export PYTHONPATH="Test" && echo "import sysfor path in sys.path: print(path)" > main.py && uv run main.py /Users/qianshuang/Project/PythonProject/PythonRelearn # 当前工作目录/Users/qianshuang/Project/PythonProject/PythonRelearn/Test # PYTHONPATH 环境变量指定的目录/Users/qianshuang/.local/share/uv/python/cpython-3.14.3-macos-aarch64-none/lib/python314.zip # 标准库的压缩包版本/Users/qianshuang/.local/share/uv/python/cpython-3.14.3-macos-aarch64-none/lib/python3.14 # 标准库 Python 源码/Users/qianshuang/.local/share/uv/python/cpython-3.14.3-macos-aarch64-none/lib/python3.14/lib-dynload # 标准库的 C 扩展二进制模块/Users/qianshuang/Project/PythonProject/PythonRelearn/.venv/lib/python3.14/site-packages # 虚拟环境中的第三方包目录各个路径在sys.path中的顺序实际上决定了搜索优先级,后续路径仅在前序路径未搜索到的情况下才会被搜索。
4.2 运行期修改
代码可以动态修改 sys.path,这种修改只影响当前解释器进程,并且只影响修改之后发生的导入。
import syssys.path.insert(0, '/tmp/project-libs')print(sys.path[0]) # /tmp/project-libssys.path.pop(0)4.3 导入冲突
导入冲突来自路径顺序。Python 按 sys.path 从前到后查找模块,先找到的名字会被加载。当前目录下如果有 json.py、typing.py
这类文件,import json 或 import typing 可能先加载本地文件,而不是标准库模块。
排查导入冲突时,先查看模块实际来源:
import jsonprint(json.__name__) # jsonprint(json.__file__) # 标准库 json 模块的实际路径print(json.__spec__.origin) # 标准库 json 模块的实际路径常见处理方式是:重命名本地冲突文件或包目录,删除残留的 __pycache__,检查 sys.path 前几个目录,避免在库代码中随意把项目目录插到搜索路径最前面。
5. 小结
模块是 .py 文件对应的命名空间对象;包是组织多个模块的目录结构。__name__ 是模块成员之一,用来区分导入使用和直接作为顶层模块运行。
包的 __init__.py 是包入口,适合放元数据、__all__ 和轻量包级导出。导入时,Python 根据 sys.path 查找模块或包,执行模块顶层代码,并把模块对象放入
sys.modules 缓存。
附录
import package.module 与 print(package.module) 的区别
在python中,import package.module后的package.module将被import作为一个字符串整体对待,而不是通过package对象导入
module成员。
比如以下代码:
(pythonrelearn) qianshuang@qianshuangdeMacBook-Air PythonRelearn % tree acme acme├── __init__.py└── config.py# acme/__init__.pyprint('loading acme package')name = 'acme'# acme/config.pyprint('loading acme.config')debug = True可以看到,acme包的__init__.py文件并没有保留对子模块config的引用,因此如果我们在代码中这样使用
# main.pyimport acmeprint(acme.config.debug) # 通过包对象 acme 访问子模块对象 config解释器会抛出错误
(pythonrelearn) qianshuang@qianshuangdeMacBook-Air PythonRelearn % uv run main.py loading acme packageTraceback (most recent call last): File "/Users/qianshuang/Project/PythonProject/PythonRelearn/main.py", line 3, in <module> print(acme.config.debug) ^^^^^^^^^^^AttributeError: module 'acme' has no attribute 'config'解决方法有两个,一个是在__init__.py中保存对子模块的引用,这样一来,在解析import acme时,解释器会递归解析对acme.config
的导入。
# acme/__init__.pyfrom .config import debugprint('loading acme package')name = 'acme'另一种方法是单独再次import acme.config
# main.pyimport acmeimport acme.configprint(acme.config.debug) # 实际上等价于 print(sys.modules['acme.config'].debug)# 而不是 print(sys.modules['acme'].config.debug)这里便有一个很玄学的问题,既然print(acme.config.debug)没办法通过acme访问到config。那为什么import acme.config能够成功呢?
这是因为当我们执行import package.module时,实际上执行的是importlib.import_module('package.module'),而不是
package.import_module('module')。
acme在import acme.config中根本不是import acme得到的包对象,而是一个包名称。 因此,import acme.config将首先通过包名查找包
acme,然后找到子模块config(而不是通过acme包对象导入config子模块)。
这也说明了,import package.module将后面的 package.module作为简单的字符串标识符对待,与print(package.module)的
对象->成员语义实际上并不相同。