Python的执行与导入机制
Python 中有两种执行方式:python foo/bar.py(脚本执行)和 python -m foo.bar(模块执行)。写 Python 时经常会遇到——哪怕是同样的代码——换一种执行方式就报错的事。这是因为两种执行方式虽然看起来在做同样的事,但却会给 sys.path[0] 和 __package__ 这两个重要变量设置不同的值。本文将简单讲解一下其中奥秘。
执行方式
先来看一个简单的例子:
1 | |
脚本执行时:
1 | |
可以看到,脚本执行需要提供的是文件路径。不论 cwd 是什么,sys.path[0] 总是 .py 文件所在目录的绝对路径,而 __package__ 总是 None。
模块执行时:
1 | |
与脚本执行不同,模块执行需要提供的是模块名。sys.path[0] 会被设置成 cwd,__package__ 会被设置成模块的所属包:
demo.module模块属于demo包module是顶层模块,不属于任何包,因此__package__为''pkg包等效于pkg.__main__模块,而后者属于pkg包
Python 按以下规则把文件名映射成模块名:
- .py 文件是一个模块(module),模块名是文件名去掉后缀(如
module.py->module) - 目录是一个包(package),包名是目录名(如
pkg->pkg)- 包也是模块
- 目录内的 .py 文件是子模块,多级模块名用
.拼接(如pkg/submodule.py->pkg.submodule)- 嵌套可以超过两级(如
pkg/subpkg/submodule.py->pkg.subpkg.submodule)
- 嵌套可以超过两级(如
- 如果
pkg目录中存在__init__.py,它会在import pkg时被执行 - 如果
pkg目录中存在__main__.py,它会在python -m pkg时被执行
sys.path
sys.path 是一个列表,它决定了 Python 在 import 时会在哪些路径搜索,类似于 Linux/Unix 中的 PATH 环境变量。其中,sys.path[0] 最为特殊,由执行方式决定,而其余的由 PYTHONPATH 环境变量、安装的依赖等决定。
在运行 python -m foo.bar 时,Python 会先将 sys.path[0] 设为 cwd,再用类似 import foo.bar 的机制去解析 foo.bar。
package
__package__ 是相对导入的锚点,也就是 from . import foo 中 . 的位置。如果 __package__ 为 None 或 '',相对导入就会失败。
再来看一个例子:
1 | |
用不同方法执行:
1 | |
脚本执行时 __package__ 是 None,没有锚点,相对导入直接失败;模块执行时,__package__ 是 'mypkg',from . import helper 等效于 import mypkg.helper,可以正常解析。
如果改用绝对导入(import helper),则会得到完全相反的结果:脚本执行成功(sys.path[0] 恰好是 mypkg 目录,能找到 helper),模块执行反而失败(sys.path[0] 是 mypkg 的父级目录,找不到 helper)。
常规包与命名空间包
一个包里可以有 __init__.py,也可以没有。如果有,它是一个常规包(regular package)。如果没有,它是一个命名空间包(namespace package)。第一个示例中,demo 目录中并没有 __init__.py,但却仍然支持 python -m demo.module,这是因为 demo 目录被当成命名空间包来处理了。