__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 模块:

    $ python helloworld.py
    Hello, world!
    
  • 作为 -m 参数传给 Python 解释器的 Python 模块或包:

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

    $ echo "import this" | python
    The Zen of Python, by Tim Peters
    
    Beautiful is better than ugly.
    Explicit is better than implicit.
    ...
    
  • 作为 -c 参数传递给 Python 解释器的 Python 代码:

    $ python -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 (如果你的函数没有返回语句,则隐含返回)。

By proactively following this convention ourselves, our module will have the same behavior when run directly (i.e. python echo.py) as it will have if we later package it as a console script entry-point in a pip-installable package.

特别的是,要小心从你的 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 。比如说。

$ python -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 是首选。

参见

See venv for an example of a package with a minimal __main__.py in the standard library. It doesn't contain a if __name__ == '__main__' block. You can invoke it with 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())

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

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

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

$ python 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 的实现。