Files

7.8 KiB
Raw Permalink Blame History

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

创建包的基本步骤:

  1. 选择一个包名并创建同名目录,例如 porty/。
  2. 在目录中添加 __init__.py,该文件可以为空。
  3. 把相关源文件放入该目录。

包作为导入命名空间

包会形成一个导入命名空间,因此导入路径变成多级形式:

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')

这些写法体现了包在组织大型代码库时的价值:模块不再漂浮在顶层,而是归属于一个明确的命名空间。

包化后的两个常见问题

把文件移入包目录后,通常会遇到两个问题:

  1. 同一个包内部模块之间的导入会失效。
  2. 直接运行包内模块作为主脚本会失效。

这两个问题都与 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应用结构。