Python 包引入规范

⚠ 转载请注明出处:作者:ZobinHuang,更新日期:Mar.28 2021


知识共享许可协议

    本作品ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。

1. 包引用的规范和原理说明

(0). 背景

    首先我们约定一下术语。在 python 中,一个 .py 文件就是一个 模块 (module)。我们使用一个文件夹,装了很多的 .py文件 (模块),然后在这个文件夹下创建一个 __init__.py 文件,这个文件夹就变成了 软件包 (packedge),此时在其它模块中就能将这个文件夹识别成一个 Python 软件包,进而 import 里面的模块。

(1) 规范1:使用绝对路径而不是相对路径

    以下面的工程结构为例,所有的源文件都被放置在工程根目录下的 (Project Root)/commer/packedge/module.py 中,比如 (Project Root)/commer/msg/content_msg.py

    我强烈建议在工程中的各个源文件内做包引用的时候,使用绝对路径来引用各个包内的模块,比如,在 (Project Root)/commer/test/queue_set_test.py 中,若要使用 (Project Root)/commer/msg/content_msg.py 这个模块,则采用以下绝对路径的方式引入。这么做有几个好处,一个是其他用户能在他们的环境中直接使用我们的工程代码,第二个是保证了工程中各个源文件中模块引用语句的统一性 (i.e. 各个文件要引用 (Project Root)/commer/msg/content_msg.py 模块的语句都是相同的),不会造成混乱。

1
2
# (Project Root)/commer/test/queue_set_test.py
from commer.msg.content_msg import content_msg

    注意到,Python在检索导入的包的范围是这样的:
        (1) 源文件所在目录
        (2) PYTHONPATH环境变量设置的目录
        (3) 标准库的目录
        (4) 任何能够找到的.pth文件的内容
        (5) 第三方扩展的site-package目录
    最重要的一点来了,用以上绝对路径导入模块/包的前提是,你已经把工程的根目录 (i.e. 上面例子中的 .../Commer/) 加入了检索的范围,否则任何尝试使用绝对路径导入包的尝试都将是失败的。我们可以使用以下代码查看当前系统检索包的路径范围:

1
2
3
# 打印检索包的路径
import sys
print(sys.path)

    如果打印出的东西里没有我们的工程的根目录,那么所有的绝对路径寻包都会报错。这里给出一种解决的办法:使用 shell 命令在 PYTHONPATH 变量中添加我们工程的目录。可以把 shell 命令封装成为脚本文件,如下所示

1
2
3
4
5
# please source this shell file
# set root path of python
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
export PYTHONPATH=$PYTHONPATH:$(dirname "$DIR")
echo "set PYTHONPATH as:" $PYTHONPATH

    注意,运行这个脚本文件必须使用 source 命令而不能是 bash 命令。这里就得扯到 进程全局变量进程局部变量 的事情,故事是这样的:
    进程可以创建局部变量,局部变量只有进程自己内部能够使用,对其他进程包括它自己创建的子进程都是不可见的。进程可以使用 export 命令把局部变量转化为全局变量,全局变量的可见范围就比较广了,当父进程创建子进程后,子进程会继承父进程的全局变量,也就是说全局变量对子进程是可见的,只不过子进程继承的是父进程刚开始全局变量的值。注意,反过来,父进程是看不见子进程自己创建的局部 & 全局变量的。
    回到我们的场景,当用户开启一个终端时,实际上操作系统是给我们创建了一个 shell 进程 A。当我们在这个 shell 进程 A 中使用 bash 来运行我们的脚本时,实际上背后的机制是是我们的 shell 进程 A 又创建了一个子 shell 进程 B 来执行这段脚本,而我们发现这段脚本做的事情是:创建了一个局部变量 PYTHONPATH 给它赋值,并且把它 export 成为全局变量,使得这个变量对 B 的子进程可见。但是我们注意到此时的 PYTHONPATH 变量完完全全是在这个子 shell 进程 B 里执行的,所以我们的 shell 进程 A 完全看不见,而且当这段脚本执行完之后,子 shell 进程 B就结束了,所以这个 PYTHONPATH 变量压根就不会影响到我们真正想操作的 shell 进程 A 的变量域,因此使用 bash 来运行这个脚本是无法成功创建全局变量 PYTHONPATH 的。
    正确的做法是使用 source 命令,sourcebash 的区别就在于 source 是直接当我们的 shell 进程 A 执行这段脚本,而不是创建子进程。因此能够成功的在 shell 进程 A 创建全局变量 PYTHONPATH 并且赋上我们想要的值。