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