__main__ ——顶层代码环境


Python 的特殊名 __main__ 用于两个重要的构造:

  1. 程序的顶层环境的名称,可用表达式 __name__ == '__main__' 来检查;以及

  2. Python 包中的文件 __main__.py

这两个机制都与 Python 模块相关——用户与它们如何交互,及它们之间如何交互——下文详述。而教程的 模块 一节则为初学者介绍了 Python 模块。

__name__ == '__main__'

当一个 Python 模块或包被导入时,__name__ 被设为模块的名称——通常为 Python 文件本身的名称去掉 .py 后缀:

>>> import configparser
>>> configparser.__name__
'configparser'

如果文件是包的一部分,则 __name__ 还将包括父包的路径:

>>> from concurrent.futures import process
>>> process.__name__
'concurrent.futures.process'

而若模块是在顶层代码环境中执行的,则其 __name__ 被设为字符串 '__main__'

什么是“顶层代码环境”?

__main__ 是顶层代码运行环境的名称。“顶层代码”是指由用户指定的最先开始运行的那一个 Python 模块。之所以它是“顶层”,是因为它将导入程序所需的所有其它模块。有时“顶层代码”被称为应用程序的 入口点

顶层代码环境可以是:

  • 交互提示符的作用域:

    >>> __name__
    '__main__'
    
  • 作为文件参数传给 Python 解释器的 Python 模块:

    $ python3 helloworld.py
    Hello, world!
    
  • -m 一起传给 Python 解释器的 Python 模块或包:

    $ python3 -m tarfile
    usage: tarfile.py [-h] [-v] (...)
    
  • Python 解释器从标准输入中读取的 Python 代码:

    $ echo "import this" | python3
    The Zen of Python, by Tim Peters
    
    Beautiful is better than ugly.
    Explicit is better than implicit.
    ...
    
  • -c 一起传给 Python 解释器的 Python 代码:

    $ python3 -c "import this"
    The Zen of Python, by Tim Peters
    
    Beautiful is better than ugly.
    Explicit is better than implicit.
    ...
    

上述每种情况中的顶层模块的 __name__ 被设为 '__main__'

作为结果,模块通过检查自己的 __name__ 可发现自己是否运行于顶层环境,使一些代码仅当模块不是被导入语句初始化的时候才执行:

if __name__ == '__main__':
    # Execute when the module is not initialized from an import statement.
    ...

参见

关于在所有情况下 __name__ 是被如何设置的,详见教程的 模块 一节。

惯用法

有些模块包含了仅供脚本使用的代码,比如解析命令行参数或从标准输入获取数据。 如果这样的模块被从不同的模块中导入,例如为了单元测试,脚本代码也会无意中执行。

这就是 if __name__ == '__main__' 代码块的用武之地。除非模块在顶层环境中被执行,否则该块内的代码不会运行。

将尽可能少的语句放在下面的 if __name___ == '__main__' 块中可以提高代码的清晰度和正确性。最常见的,一个名为 main 的函数封装了程序的主要行为:

# echo.py

import shlex
import sys

def echo(phrase: str) -> None:
   """A dummy wrapper around print."""
   # for demonstration purposes, you can imagine that there is some
   # valuable and reusable logic inside this function
   print(phrase)

def main() -> int:
    """Echo the input arguments to standard output"""
    phrase = shlex.join(sys.argv)
    echo(phrase)
    return 0

if __name__ == '__main__':
    sys.exit(main())  # next section explains the use of sys.exit

请注意,如果模块没有将代码封装在 main 函数内,而是直接放在 if __name__ == '__main__' 块内,那么这个 phrase 变量对整个模块来说就是全局变量。 这很容易出错,因为模块内的其他函数可能会无意中使用全局变量而不是局部名称。 一个 main 函数解决了这个问题。

使用 main 函数有一个额外的好处,就是 echo 函数本身是孤立的,可以在其他地方导入。当 echo.py 被导入时,echomain 函数将被定义,但它们都不会被调用,因为 __name__ != '__main__'

打包考量

main 函数经常被用来创建命令行工具,把它们指定为控制台脚本的入口点。 当这样做时,pip 将函数调用插入到模板脚本中,其中 main 的返回值被传递到 sys.exit() 。例如:

sys.exit(main())

由于 main 调用被包裹在 sys.exit() 中,期望你的函数将返回一些可被 sys.exit() 作为输入而接受的值;通常为一个整数或 None (如果你的函数没有返回语句,则隐含返回)。

通过主动遵循这一惯例,我们的模块在直接运行时(即 python3 echo.py )会有相同的行为,如果我们以后把它打包成可用 pip 安装的包中的控制台脚本入口,它也会有相同的行为。

特别的是,要小心从你的 main 函数中返回字符串。 sys.exit() 将把一个字符串参数解释为失败信息,所以你的程序将有一个 1 的退出代码,表示失败。并且这个字符串将被写入 sys.stderr 。 前面的 echo.py 例子举例说明了使用 sys.exit(main()) 的约定。

参见

Python 打包用户指南 包含了一系列关于如何用现代工具分发和安装 Python 包的教程和参考资料。

Python 包中的 __main__.py

如果你不熟悉Python包,请参阅本教程的 一节。最常见的是, __main__.py 文件被用来为一个包提供命令行接口。假设有下面这个虚构的包,"bandclass":

bandclass
  ├── __init__.py
  ├── __main__.py
  └── student.py

当使用 -m 标志从命令行直接调用软件包本身时,将执行 __main__.py 。比如说。

$ python3 -m bandclass

这个命令将导致 __main__.py 的运行。你如何利用这一机制将取决于你所编写的软件包的性质,但在这个假设的案例中,允许教师搜索学生可能是有意义的:

# bandclass/__main__.py

import sys
from .student import search_students

student_name = sys.argv[2] if len(sys.argv) >= 2 else ''
print(f'Found student: {search_students(student_name)}')

注意, from .student import search_students 是一个相对导入的例子。 这种导入方式可以在引用一个包内的模块时使用。 更多细节,请参见教程 模块 中的 相对导入 一节。

惯用法

__main__.py 的内容通常不是用 if __name__ == '__main__' 区块围起来的。 相反,这些文件保持简短,功能从其他模块执行。 那些其他模块可以很容易地进行单元测试,并且可以适当地重复使用。

如果使用,一个 if __name__ == '__main__' 区块仍然会像预期的那样对包内的 __main__.py 文件起作用,因为如果导入,它的 __name__ 属性将包括包的路径:

>>> import asyncio.__main__
>>> asyncio.__main__.__name__
'asyncio.__main__'

但这对 .zip 文件的根目录中的 __main__.py 文件不起作用。 因此,为了保持一致性,像下面提到的 venv 这样的最小 __main__.py 是首选。

参见

请参阅 venv 以获取标准库中具有最小化 __main__.py 的软件包示例。 它不包含 if __name__ == '__main__' 代码块。 你可以用 python -m venv [directory] 来发起调用。

参见 runpy 以了解更多关于 -m 标志对解释器可执行包的细节。

参见 zipapp 了解如何运行打包成 .zip 文件的应用程序。在这种情况下,Python 会在归档文件的根目录下寻找一个 __main__.py 文件。

import __main__

不管 Python 程序是用哪个模块启动的,在同一程序中运行的其他模块可以通过导入 __main__ 模块来导入顶级环境的范围 ( namespace )。这并不是导入一个 __main__.py 文件,而是导入使用特殊名称 '__main__' 的哪个模块。

下面是一个使用 __main__ 命名空间的模块的例子:

# namely.py

import __main__

def did_user_define_their_name():
    return 'my_name' in dir(__main__)

def print_user_name():
    if not did_user_define_their_name():
        raise ValueError('Define the variable `my_name`!')

    if '__file__' in dir(__main__):
        print(__main__.my_name, "found in file", __main__.__file__)
    else:
        print(__main__.my_name)

该模块的用法示例如下:

# start.py

import sys

from namely import print_user_name

# my_name = "Dinsdale"

def main():
    try:
        print_user_name()
    except ValueError as ve:
        return str(ve)

if __name__ == "__main__":
    sys.exit(main())

现在,如果我们启动我们的程序,结果会是这样的:

$ python3 start.py
Define the variable `my_name`!

该程序的退出代码为 1 ,表明有错误。取消对 my_name = "Dinsdale" 这一行的注释,就可以修复程序,现在它的退出状态代码为 0 ,表示成功。

$ python3 start.py
Dinsdale found in file /path/to/start.py

请注意,导入 __main__ 不会导致无意中运行旨在用于脚本的顶层代码的问题,这些代码被放在模块 startif __name__ == "__main__" 块中。为什么这样做?

Python 解释器启动时会在 sys.modules 中插入一个空的 __main__ 模块,并通过运行顶层代码来填充它。 在我们的例子中这就是 start 模块,它逐行运行并导入 namely。 反过来,namely 会导入 __main__ (这其实是 start)。 这就是一个导入循环! 幸运的是,由于部分填充的 __main__ 模块存在于 sys.modules 中,Python 会将其传递给 namely。 请参阅导入系统参考资料中 有关 __main__ 的特别考量 来了解其中的详情。

Python REPL 是另一个 "顶层环境 "的例子,所以在 REPL 中定义的任何东西都成为 __main__ 范围的一部分:

>>> import namely
>>> namely.did_user_define_their_name()
False
>>> namely.print_user_name()
Traceback (most recent call last):
...
ValueError: Define the variable `my_name`!
>>> my_name = 'Jabberwocky'
>>> namely.did_user_define_their_name()
True
>>> namely.print_user_name()
Jabberwocky

注意,在这种情况下, __main__ 范围不包含 __file__ 属性,因为它是交互式的。

__main__ 范围用于 pdbrlcompleter 的实现。