如何组织Python软件包

前言

就如 =stackoverflow= 上问题 how-to-do-relative-imports-in-python 所描述,最近也遇到了Python软件包组织结构的烦恼。

查资料的结果是 http://blog.habnab.it/blog/2013/07/21/python-packages-and-you/ 说的比较明白,这就做个简单地笔记.

relative和absolute导入

=absolute-import=,比如:

1
from P.A import B

=relative-import=,比如:

1
from .A import B

应该尽量保持使用软件包绝对路径的方式,一方面能够让代码更加清晰明了,另一方面相对路径方式需要比较高版本的Python才支持。

覆盖标准库问题

如果自定义的软件包与标准库中某个软件重名,则在导入的时候,可能导入的不是自己想要的.

比如标准库中有A,而自定义一个软件也叫A,则在A中可以强制绝对路径导入:

1
from __future__ import absolute_import

这样,在A中导入A的时候,找的是标准库里面的软件包A,如 =pip/pip/_vendor/init.py=:

1
2
3
4
5
6
7
8
"""
pip._vendor is for vendoring dependencies of pip to prevent needing pip to
depend on something external.
Files inside of pip._vendor should be considered immutable and should only be
updated to versions from upstream.
"""
from __future__ import absolute_import

在自定义包中的 =init.py= 文件中应该总是加入这行.

独立运行脚本或软件包

如果要运行一个软件包A,则应采用这种方式:

1
$ python -m A

只需要创建 =A/main.py= ,则上面这种方式会自动调用此文件;通过这种方式,可以很方便的运行软件包。

而对于软件包中某个模块B.py,则常常会这样测试:

1
2
if __name__ == "__main__":
#....

这种方式的问题,一方面,当 =name= 等于 =main= 的时候,包中模块的相对位置关系都没有了,很可能导致报错;另一方面,不好维护。

为了方便维护与测试,可以在C.py中:

1
2
3
4
5
6
def main(args=None):
#...
if __name__ == "__main__":
import sys
sys.exit(main())

运行的时候也不要直接(=package 是 None=):

1
$ python A/B/C.py

为了保持包中模块的相对位置关系,应该(=package是A.B=):

1
$ python -m A.B.C

通过这样的组织方式,可以很好地处理各种包导入引起的问题,比如使用了相对路径导入.

总结:

  • 如果是一个软件包目录A,则创建 =A/main.py= ,在 =python -m A= 的时候寻找 =A/main.py=
  • 如果是一个软件包目录A,则创建 =A/init.py= ,在 =import A= 的时候会寻找 =A/init.py=
  • 如果是一个软件包子目录,则创建 =A/B/main.py= ,在 =python -m A.B= 的时候寻找 =A/B/mian.py=
  • 如果是一个软件包子目录,则创建 =A/B/init.py= ,在 =import A.B= 的时候寻找 =A/B/init.py=
  • 如果是一个模块C,则不管是 =import A.B.C= 或者 =python -m A.B.C= 寻找具体的 =A/B/C.py=

文档目录

最好提供 =doc/= 目录用于存放文档,至少应该有一个 =README= .

其它常见问题

  • 不要以设置 =PYTHONPATH= 变量的方式让程序运行;只要软件包组织合理,不需要这样。
  • 不要在代码中修改 =sys.path=
  • 项目根目录里面应该包含一个Python软件包,而不是以一个软件包作为项目根目录,否则setup.py可能无法工作。比如软件包A,存放源码的地方可能会是 =A/lib/= 或 =A/src/=, 这样都不好,应该采用 =A/A/=. 如pip的文件结构:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    pip/AUTHORS.txt
    pip/CHANGES.txt
    pip/LICENSE.txt
    pip/MANIFEST.in
    pip/README.rst
    pip/contrib/
    pip/docs/
    pip/setup.cfg
    pip/setup.py
    pip/tasks/
    pip/tests/
    pip/tox.ini
    pip/pip/__init__.py
    pip/pip/__main__.py
    pip/pip/_vendor/
    pip/pip/basecommand.py
    pip/pip/baseparser.py
    pip/pip/cmdoptions.py
    pip/pip/commands/
    pip/pip/compat/
    pip/pip/download.py
    pip/pip/exceptions.py
    pip/pip/index.py
    pip/pip/locations.py
    pip/pip/pep425tags.py
    pip/pip/req/
    pip/pip/status_codes.py
    pip/pip/utils/
    pip/pip/vcs/
    pip/pip/wheel.py

pip/pip/main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from __future__ import absolute_import
import sys
# If we are running from a wheel, add the wheel to sys.path
# This allows the usage python pip-*.whl/pip install pip-*.whl
if __package__ == '':
import os
# __file__ is pip-*.whl/pip/__main__.py
# first dirname call strips of '/__main__.py', second strips off '/pip'
# Resulting path is the name of the wheel itself
# Add that to sys.path so we can import pip
path = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, path)
import pip
if __name__ == '__main__':
sys.exit(pip.main())

后记

遵循这几条简单地规则,就可以在创建Python软件包的时候省去很多烦恼.

资料

吴羽舒 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!