使用 -m 选项执行 Python 代码的区别
Python 解释器有一个 -m
模块 选项,它的作用是“把库模块 模块 当作脚本来运行”。
这里有一个 Python 代码文件 a.py:
if __name__ == "__main__":
print __package__
print __name__
我测试了 python -m a
,得到的结果是
"" <-- Empty String
__main__
而 python a.py
返回的是
None <-- None
__main__
对我来说,这两种调用方式似乎是一样的,除了用 -m
选项调用时,__package__ 的值不是 None。
有趣的是,用 python -m runpy a
运行时,我得到的结果和 python -m a
一样,并且 Python 模块会被编译成 a.pyc 文件。
这两种调用方式有什么(实际)区别?它们各自的优缺点是什么?
另外,David Beazley 的《Python 重要参考》解释说:“-m 选项将库模块作为脚本运行,这个脚本在执行主脚本之前会在 __main__ 模块内执行。”这是什么意思呢?
3 个回答
使用 -m 选项来运行一个模块(或包)作为脚本,主要是为了简化部署,特别是在 Windows 系统上。你可以把脚本安装在 Python 库的同一个地方,那里通常是模块存放的地方,这样就不会弄得 PATH 或者全局可执行目录(比如 ~/.local)乱七八糟的。在 Windows 上,用户脚本的目录真的是难找得要命。
这样你只需要输入 -m,Python 就能自动找到这个脚本。例如,python -m pip
会找到与执行它的 Python 解释器相同的 pip。如果不使用 -m,用户如果安装了多个 Python 版本,那哪个才是“全局”的 pip 呢?
如果用户更喜欢“经典”的命令行脚本入口,可以很容易地在 PATH 中添加一些小脚本,或者在安装时通过 setup.py 中的 entry_points 参数让 pip 自动创建这些入口。
所以只需要检查 __name__ == '__main__'
,然后忽略其他不太可靠的实现细节就可以了。
使用 -m 选项执行 Python 代码与不使用的区别
使用 -m
标志。
如果你在运行一个脚本,结果差不多,但如果你在开发一个包,不加 -m
标志的话,就没办法正确导入模块了,尤其是当你想把某个子包或模块当作程序的主入口来运行时(相信我,我试过了)。
文档说明
就像关于 -m 标志的文档所说:
在 sys.path 中查找指定的模块,并将其内容作为
__main__
模块执行。
还有:
与 -c 选项一样,当前目录会被添加到 sys.path 的开头。
所以
python -m pdb
大致等同于
python /usr/lib/python3.5/pdb.py
(假设你当前目录下没有叫做 pdb.py 的包或脚本)
解释:
这种行为“故意与”脚本类似。
许多标准库模块在作为脚本执行时会调用其中的代码。比如timeit 模块:
一些 Python 代码是打算作为模块运行的:(我觉得这个例子比命令行选项文档中的例子更好)
$ python -m timeit '"-".join(str(n) for n in range(100))'
10000 loops, best of 3: 40.3 usec per loop
$ python -m timeit '"-".join([str(n) for n in range(100)])'
10000 loops, best of 3: 33.4 usec per loop
$ python -m timeit '"-".join(map(str, range(100)))'
10000 loops, best of 3: 25.2 usec per loop
-m 命令行选项 - python -m modulename 会在标准库中找到一个模块并调用它。例如,
python -m pdb
相当于python /usr/lib/python2.4/pdb.py
后续问题
此外,David Beazley 的《Python Essential Reference》解释为:“-m 选项将库模块作为脚本运行,该脚本在主脚本执行之前在
__main__
模块内执行。”
这意味着你可以用 import 语句查找的任何模块都可以作为程序的入口点运行——前提是它有一个代码块,通常在最后,包含 if __name__ == '__main__':
。
-m
不添加当前目录到路径:
这里有个评论提到:
-m 选项还会将当前目录添加到 sys.path,这显然是一个安全问题(见:预加载攻击)。这种行为类似于 Windows 中的库搜索顺序(在最近加强之前)。可惜 Python 没有跟上这个趋势,也没有提供简单的方法来禁用将 . 添加到 sys.path。
好吧,这展示了可能的问题——(在 Windows 中去掉引号):
echo "import sys; print(sys.version)" > pdb.py
python -m pdb
3.5.2 |Anaconda 4.1.1 (64-bit)| (default, Jul 5 2016, 11:41:13) [MSC v.1900 64 bit (AMD64)]
使用 -I
标志来锁定生产环境(在 3.4 版本中新增):
python -Im pdb
usage: pdb.py [-c command] ... pyfile [arg] ...
etc...
来自文档:
-I
以隔离模式运行 Python。这也意味着 -E 和 -s。在隔离模式下,sys.path 中既不包含脚本的目录,也不包含用户的 site-packages 目录。所有 PYTHON* 环境变量也会被忽略。还可以施加进一步的限制,以防止用户注入恶意代码。
__package__
是干嘛的?
它使得显式相对导入成为可能,虽然这与这个问题并没有特别关系——可以看看这里的回答:Python 中的 "__package__" 属性有什么用?
当你使用 -m
这个命令行选项 时,Python 会帮你导入一个模块或包,然后把它当作脚本来运行。如果你不使用 -m
选项,那么你指定的文件就只是一个普通的脚本。
这个区别在你尝试运行一个包的时候非常重要。下面这两种情况有很大的不同:
python foo/bar/baz.py
和
python -m foo.bar.baz
在后者的情况下,foo.bar
被导入,相关的导入会正确地以 foo.bar
作为起点。
演示:
$ mkdir -p test/foo/bar
$ touch test/foo/__init__.py
$ touch test/foo/bar/__init__.py
$ cat << EOF > test/foo/bar/baz.py
> if __name__ == "__main__":
> print __package__
> print __name__
>
> EOF
$ PYTHONPATH=test python test/foo/bar/baz.py
None
__main__
$ PYTHONPATH=test python -m foo.bar.baz
foo.bar
__main__
因此,当使用 -m
选项时,Python 需要真正关注包。一个普通的脚本永远不能被视为一个包,所以 __package__
被设置为 None
。
但是,如果你用 -m
在一个包内部运行一个包或模块,那么至少有可能被视为一个包,这样 __package__
变量就会被设置为一个字符串值;在上面的演示中,它被设置为 'foo.bar'
,而对于不在包内的普通模块,它则被设置为空字符串。
至于 __main__
模块,Python 在运行脚本时会像导入普通模块那样导入它。一个新的模块对象会被创建来保存全局命名空间,并存储在 sys.modules['__main__']
中。这就是 __name__
变量所指的内容,它是那个结构中的一个键。
对于包,你可以在包内创建一个 __main__.py
模块,当你运行 python -m package_name
时,这个模块会被执行;实际上,这也是你唯一能以脚本形式运行包的方式:
$ PYTHONPATH=test python -m foo.bar
python: No module named foo.bar.__main__; 'foo.bar' is a package and cannot be directly executed
$ cp test/foo/bar/baz.py test/foo/bar/__main__.py
$ PYTHONPATH=test python -m foo.bar
foo.bar
__main__
所以,当你用 -m
运行一个包时,Python 会在这个包中寻找一个 __main__
模块,并把它当作脚本来执行。它的名字仍然被设置为 '__main__'
,而模块对象仍然存储在 sys.modules['__main__']
中。