Python的执行与导入机制

Python 中有两种执行方式:python foo/bar.py(脚本执行)和 python -m foo.bar(模块执行)。写 Python 时经常会遇到——哪怕是同样的代码——换一种执行方式就报错的事。这是因为两种执行方式虽然看起来在做同样的事,但却会给 sys.path[0]__package__ 这两个重要变量设置不同的值。本文将简单讲解一下其中奥秘。

执行方式

先来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ tree demo/
demo/
├── module.py
└── pkg/
├── __init__.py
└── __main__.py

$ cat demo/module.py
import sys
print(f"sys.path[0] = {sys.path[0]!r}")
print(f"__package__ = {__package__!r}")
print("hello from module.py")

$ cat demo/pkg/__init__.py
$ cat demo/pkg/__main__.py
import sys
print(f"sys.path[0] = {sys.path[0]!r}")
print(f"__package__ = {__package__!r}")
print("hello from pkg/__main__.py")

脚本执行时:

1
2
3
4
5
6
7
8
9
$ python demo/module.py
sys.path[0] = '/path/to/demo'
__package__ = None
hello from module.py

$ cd demo && python module.py
sys.path[0] = '/path/to/demo'
__package__ = None
hello from module.py

可以看到,脚本执行需要提供的是文件路径。不论 cwd 是什么,sys.path[0] 总是 .py 文件所在目录的绝对路径,而 __package__ 总是 None

模块执行时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python -m demo.module
sys.path[0] = '/path/to'
__package__ = 'demo'
hello from module.py

$ cd demo && python -m module
sys.path[0] = '/path/to/demo'
__package__ = ''
hello from module.py

$ cd demo && python -m pkg
sys.path[0] = '/path/to/demo'
__package__ = 'pkg'
hello from pkg/__main__.py

与脚本执行不同,模块执行需要提供的是模块名。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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ tree mypkg/
mypkg/
├── __init__.py
├── __main__.py
└── helper.py

$ cat mypkg/__main__.py
import sys
print(f"__package__ = {__package__!r}")
from . import helper
helper.greet()

$ cat mypkg/helper.py
def greet():
print("hello from helper")

用不同方法执行:

1
2
3
4
5
6
7
$ python mypkg/__main__.py
__package__ = None
ImportError: attempted relative import with no known parent package

$ python -m mypkg
__package__ = 'mypkg'
hello from helper

脚本执行时 __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 目录被当成命名空间包来处理了。

参考资料


Python的执行与导入机制
https://tomzhu.site/2026/06/24/Python的执行与导入机制/
作者
Tom Zhu
发布于
2026年6月24日
许可协议