7.8 KiB
doc_type, full_text
| doc_type | full_text |
|---|---|
| short | sources/01_Packages.md |
9.1 Packages 总结
本文介绍如何把一组 Python 模块组织成包(package),以及包化后在导入、脚本运行和应用目录结构上的关键变化。核心主题包括:Python模块与包、Python导入机制、Python应用结构。
模块与包
任何 Python 源文件都是一个模块:
# foo.py
def grok(a):
...
def spam(b):
...
使用 import foo 会加载并执行该模块,然后通过模块名访问其中的对象:
import foo
a = foo.grok(2)
b = foo.spam('Hello')
当程序变大时,不适合把所有 .py 文件都放在顶层目录。更常见的做法是把相关模块放入一个包目录中:
porty/
__init__.py
pcost.py
report.py
fileparse.py
创建包的基本步骤:
- 选择一个包名并创建同名目录,例如
porty/。 - 在目录中添加
__init__.py,该文件可以为空。 - 把相关源文件放入该目录。
包作为导入命名空间
包会形成一个导入命名空间,因此导入路径变成多级形式:
import porty.report
port = porty.report.read_portfolio('port.csv')
也可以使用其他导入写法:
from porty import report
port = report.read_portfolio('portfolio.csv')
from porty.report import read_portfolio
port = read_portfolio('portfolio.csv')
这些写法体现了包在组织大型代码库时的价值:模块不再漂浮在顶层,而是归属于一个明确的命名空间。
包化后的两个常见问题
把文件移入包目录后,通常会遇到两个问题:
- 同一个包内部模块之间的导入会失效。
- 直接运行包内模块作为主脚本会失效。
这两个问题都与 Python导入机制 和 sys.path 有关。
问题一:包内导入必须调整
假设目录结构如下:
porty/
__init__.py
pcost.py
report.py
fileparse.py
原先在 report.py 中可能写:
import fileparse
包化后,这种写法会失败,因为 fileparse 不再是顶层模块,而是 porty 包中的子模块。
应改成绝对导入:
from porty import fileparse
或者使用包相对导入:
from . import fileparse
如果原来写的是:
from fileparse import parse_csv
则可改成:
from .fileparse import parse_csv
相对导入使用 . 表示当前包,优点是包名将来改变时,内部导入不需要全部重写。
问题二:不能直接运行包内脚本
包化后,直接运行包内模块通常会失败:
python porty/pcost.py
原因是此时 Python 把该文件当作单独脚本运行,不能正确识别它所在的包结构,sys.path 和包上下文不符合预期,导致导入失败。
正确做法是使用 -m 以模块方式运行:
python -m porty.pcost
如果需要传入参数,也可以这样运行:
python3 -m porty.report portfolio.csv prices.csv txt
这会让 Python 按照包模块路径解析 porty.report,从而正确处理包内导入。
__init__.py 的作用
__init__.py 的主要作用是把包内模块“缝合”在一起,并决定包顶层暴露哪些名称。
例如:
# porty/__init__.py
from .pcost import portfolio_cost
from .report import portfolio_report
这样使用者可以直接从包顶层导入函数:
from porty import portfolio_cost
portfolio_cost('portfolio.csv')
而不必写成:
from porty import pcost
pcost.portfolio_cost('portfolio.csv')
因此,__init__.py 不只是包标记文件,也可以作为包的公共接口入口。
顶层脚本方案
虽然 python -m package.module 是推荐方式,但对用户来说可能不够自然。另一种做法是在包外创建一个顶层脚本,由它调用包内逻辑。
例如:
#!/usr/bin/env python3
# pcost.py
import porty.pcost
import sys
porty.pcost.main(sys.argv)
或:
#!/usr/bin/env python3
# print-report.py
import sys
from porty.report import main
main(sys.argv)
顶层脚本应放在包目录外:
pcost.py # 顶层脚本
porty/ # 包目录
__init__.py
pcost.py
这样脚本负责处理命令行入口,包负责提供可复用的库代码。
推荐应用结构
本文推荐一种常见应用目录组织方式:
porty-app/
README.txt
script.py # 顶层脚本
porty/
__init__.py
pcost.py
report.py
fileparse.py
其中:
porty-app/是整个应用的容器。README.txt、数据文件、示例、脚本等放在顶层。porty/只放库代码。- 顶层脚本位于包目录外部。
更完整的练习结构为:
porty-app/
portfolio.csv
prices.csv
print-report.py
README.txt
porty/
__init__.py
fileparse.py
follow.py
pcost.py
portfolio.py
report.py
stock.py
tableformat.py
ticker.py
typedproperty.py
这种结构清晰地区分了应用外壳和可复用库代码,是 Python应用结构 的重要实践。
练习 9.1:创建简单包
练习要求把已有程序和支持模块统一放入 porty/ 包中:
porty/
__init__.py
fileparse.py
follow.py
pcost.py
portfolio.py
report.py
stock.py
tableformat.py
ticker.py
typedproperty.py
然后删除旧的 __pycache__,重新测试导入:
>>> import porty.report
>>> import porty.pcost
>>> import porty.ticker
如果导入失败,需要把原先的顶层导入改成包相对导入,例如:
from . import fileparse
或:
from .fileparse import parse_csv
练习 9.2:创建应用目录
练习要求创建 porty-app/,并把 porty/ 包移动进去,同时复制测试数据和 README:
porty-app/
portfolio.csv
prices.csv
README.txt
porty/
__init__.py
fileparse.py
follow.py
pcost.py
portfolio.py
report.py
stock.py
tableformat.py
ticker.py
typedproperty.py
运行时应位于 porty-app/ 顶层目录:
cd porty-app
python3 -m porty.report portfolio.csv prices.csv txt
这体现了一个重要原则:运行包内模块时,要从应用顶层目录启动,并使用模块路径而非文件路径。
练习 9.3:创建顶层脚本
为避免用户直接使用 python -m,练习要求创建顶层脚本 print-report.py:
#!/usr/bin/env python3
# print-report.py
import sys
from porty.report import main
main(sys.argv)
该脚本放在 porty-app/ 顶层,运行方式为:
python3 print-report.py portfolio.csv prices.csv txt
最终形成的结构中,顶层脚本负责命令行入口,porty/ 包负责业务逻辑。
关键结论
- Python 源文件是模块,目录加
__init__.py可形成包。 - 包提供命名空间,使大型代码更容易组织。
- 包内模块之间的导入应使用包路径或相对导入。
- 不应直接用文件路径运行包内模块,应使用
python -m package.module。 - 可在包外创建顶层脚本,作为更友好的命令行入口。
- 应用目录应把库代码、脚本、数据和文档分层组织。
__init__.py可用于定义包的顶层公共接口。
相关概念:Python模块与包、Python导入机制、Python相对导入、Python命令行入口、Python应用结构。