目录 | 上一节 (8.3 调试) | 下一节 (9.2 第三方包)

9.1 包

如果编写一个较大的程序,我们并不真的想在顶层将其组织为一个个独立文件的大型集合。本节对包(package)进行介绍。

模块

任何一个 Python 源文件称为一个模块(module)。

  1. # foo.py
  2. def grok(a):
  3. ...
  4. def spam(b):
  5. ...

一条 import 语句加载并执行 一个模块。

  1. # program.py
  2. import foo
  3. a = foo.grok(2)
  4. b = foo.spam('Hello')
  5. ...

包 vs 模块

对于较大的代码集合,通常将模块组织到包中。

  1. # From this
  2. pcost.py
  3. report.py
  4. fileparse.py
  5. # To this
  6. porty/
  7. __init__.py
  8. pcost.py
  9. report.py
  10. fileparse.py

首先,选择一个名字并用该名字创建顶级目录。如上述的 porty (显然,第一步最重要的是选择名字)。

接着,添加 __init__.py 文件到该目录中。__init__.py 文件可以是一个空文件。

最后,把源文件放到该目录中。

使用包

包用作导入的命名空间。

这意味着现在有了多级导入。

  1. import porty.report
  2. port = porty.report.read_portfolio('port.csv')

导入语句还有其它变体:

  1. from porty import report
  2. port = report.read_portfolio('portfolio.csv')
  3. from porty.report import read_portfolio
  4. port = read_portfolio('portfolio.csv')

两个问题

这种方法存在两个主要的问题:

  • 同一包内不同文件之间的导入无效。
  • 包中的主脚本无效。

因此,基本上一切导入都是无效的,但是,除此之外,程序还是可以工作的。

问题:导入

现在,在导入的时候,同一包内的不同文件之间的导入必须包含包名。请记住这个结构:

  1. porty/
  2. __init__.py
  3. pcost.py
  4. report.py
  5. fileparse.py

根据上述规则(同一包内的不同文件之间的导入必须包含包名)修改后的导入示例:

  1. # report.py
  2. from porty import fileparse
  3. def read_portfolio(filename):
  4. return fileparse.parse_csv(...)

所有的导入都是绝对的,而不是相对的。

  1. # report.py
  2. import fileparse # BREAKS. fileparse not found
  3. ...

相对导入

除了使用包名直接导入,还可以使用使用 . 引用当前的包。

  1. # report.py
  2. from . import fileparse
  3. def read_portfolio(filename):
  4. return fileparse.parse_csv(...)

语法:

  1. from . import modname

使用上述语法使得重命名包变得容易。

问题:主脚本

将包内的子模块作为主脚本运行会导致程序中断:

  1. bash $ python porty/pcost.py # BREAKS
  2. ...

原因:你正在运行单个脚本,而 Python 不知道包的其余部分(sys.path 是错误的)。

所有的导入都会中断。要想解决这个问题,需要以不同的方式运行程序,可以使用 -m 选项。

  1. bash $ python -m porty.pcost # WORKS
  2. ...

__init__.py 文件

该文件的主要目的是将模块组织在一起。

例如:

  1. # porty/__init__.py
  2. from .pcost import portfolio_cost
  3. from .report import portfolio_report

这使得导入的时候名字出现在顶层。

  1. from porty import portfolio_cost
  2. portfolio_cost('portfolio.csv')

而不是使用多级导入:

  1. from porty import pcost
  2. pcost.portfolio_cost('portfolio.csv')

脚本的另一种解决方案

如前所述,需要使用 -m package.module 运行包内的脚本。

  1. bash % python3 -m porty.pcost portfolio.csv

还有另一种选择:编写一个新的顶级脚本。

  1. #!/usr/bin/env python3
  2. # pcost.py
  3. import porty.pcost
  4. import sys
  5. porty.pcost.main(sys.argv)

脚本位于包外面。目录结构如下:

  1. pcost.py # top-level-script
  2. porty/ # package directory
  3. __init__.py
  4. pcost.py
  5. ...

应用结构

代码组织和文件结构是应用程序可维护性的关键。

对于 Python 而言,没有“放之四海而皆准”的方法,但是一个适用于多种问题的结构就是这样:

  1. porty-app/
  2. README.txt
  3. script.py # SCRIPT
  4. porty/
  5. # LIBRARY CODE
  6. __init__.py
  7. pcost.py
  8. report.py
  9. fileparse.py

顶级 porty-app 目录是所有其他内容的容器——这些内容包括文档,顶级脚本,用例等。

同样,顶级脚本(如果有)需要放置在代码包之外(包的上一层)。

  1. #!/usr/bin/env python3
  2. # porty-app/script.py
  3. import sys
  4. import porty
  5. porty.report.main(sys.argv)

练习

此时,我们有了一个包含多个程序的目录:

  1. pcost.py # computes portfolio cost
  2. report.py # Makes a report
  3. ticker.py # Produce a real-time stock ticker

同时,还有许多具有各种功能的支持模块:

  1. stock.py # Stock class
  2. portfolio.py # Portfolio class
  3. fileparse.py # CSV parsing
  4. tableformat.py # Formatted tables
  5. follow.py # Follow a log file
  6. typedproperty.py # Typed class properties

在本次练习中,我们将整理这些代码并将它们放入一个通用包中。

练习 9.1:创建一个简单的包

请创建一个名为 porty 的目录并将上述所有的 Python 文件放入其中。另外,在 porty 目录中创建一个空的 __init__.py 文件。最后,文件目录看起来像这样:

  1. porty/
  2. __init__.py
  3. fileparse.py
  4. follow.py
  5. pcost.py
  6. portfolio.py
  7. report.py
  8. stock.py
  9. tableformat.py
  10. ticker.py
  11. typedproperty.py

请将 porty 目录中的 __pycache__ 目录移除。该目录包含了之前预编译的 Python 模块。我们想重新开始。

尝试导入包中的几个模块:

  1. >>> import porty.report
  2. >>> import porty.pcost
  3. >>> import porty.ticker

如果这些导入失败,请进入到合适的文件中解决模块导入问题,使其能够包括相对导入。例如,import fileparse 语句可以像下面这样进行修改:

  1. # report.py
  2. from . import fileparse
  3. ...

如果有类似于 from fileparse import parse_csv 这样的语句,请像下面这样修改代码:

  1. # report.py
  2. from .fileparse import parse_csv
  3. ...

练习 9.2:创建应用目录

对应用而言,将所有代码放到“包”中通常是不够的。有时,支持文件,文档,脚本等文件需要放到 porty/ 目录之外。

请创建一个名为 porty-app 的新目录。然后将我们在练习 9.1 中创建的 porty 目录移动到 porty-app 目录中。接着,复制测试文件 Data/portfolio.csvData/prices.csvporty-app 目录。另外,在 porty-app 目录下创建一个 README.txt 文件,该文件包含一些有关自己的信息。现在,代码的组织结构像下面这样:

  1. porty-app/
  2. portfolio.csv
  3. prices.csv
  4. README.txt
  5. porty/
  6. __init__.py
  7. fileparse.py
  8. follow.py
  9. pcost.py
  10. portfolio.py
  11. report.py
  12. stock.py
  13. tableformat.py
  14. ticker.py
  15. typedproperty.py

要运行代码,需要确保你现在正在顶级目录 porty-app/ 下。例如,从终端运行:

  1. shell % cd porty-app
  2. shell % python3
  3. >>> import porty.report
  4. >>>

尝试将之前的脚本作为主程序运行:

  1. shell % cd porty-app
  2. shell % python3 -m porty.report portfolio.csv prices.csv txt
  3. Name Shares Price Change
  4. ---------- ---------- ---------- ----------
  5. AA 100 9.22 -22.98
  6. IBM 50 106.28 15.18
  7. CAT 150 35.46 -47.98
  8. MSFT 200 20.89 -30.34
  9. GE 95 13.48 -26.89
  10. MSFT 50 20.89 -44.21
  11. IBM 100 106.28 35.84
  12. shell %

练习 9.3:顶级脚本

使用 python -m 命令通常有点怪异。可能需要编写一个顶级脚本来处理奇怪的包。请创建一个生成上述报告的脚本 print-report.py

  1. #!/usr/bin/env python3
  2. # print-report.py
  3. import sys
  4. from porty.report import main
  5. main(sys.argv)

然后把脚本 print-report.py放到顶级目录 porty-app/ 中。并确保可以在 porty-app/ 目录下运行它:

  1. shell % cd porty-app
  2. shell % python3 print-report.py portfolio.csv prices.csv txt
  3. Name Shares Price Change
  4. ---------- ---------- ---------- ----------
  5. AA 100 9.22 -22.98
  6. IBM 50 106.28 15.18
  7. CAT 150 35.46 -47.98
  8. MSFT 200 20.89 -30.34
  9. GE 95 13.48 -26.89
  10. MSFT 50 20.89 -44.21
  11. IBM 100 106.28 35.84
  12. shell %

最后,代码的组织结构应该下面这样:

  1. porty-app/
  2. portfolio.csv
  3. prices.csv
  4. print-report.py
  5. README.txt
  6. porty/
  7. __init__.py
  8. fileparse.py
  9. follow.py
  10. pcost.py
  11. portfolio.py
  12. report.py
  13. stock.py
  14. tableformat.py
  15. ticker.py
  16. typedproperty.py

目录 | 上一节 (8.3 调试) | 下一节 (9.2 第三方包)