__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__ 來發現它是否在頂層環境中執行,這允許一種常見的慣用法,在模組不是從 import 語句初始化時有條件地執行程式碼。

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(如果您的函式沒有 return 語句,則會隱式返回)。

透過主動遵循我們自己的約定,我們的模組在直接執行時(即 python 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。例如:

$ python -m bandclass

此命令將導致執行 __main__.py。如何利用此機制取決於您編寫的包的性質,但在這種假設情況下,允許教師搜尋學生可能是有意義的。

# bandclass/__main__.py

import sys
from .student import search_students

student_name = sys.argv[1] 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 檔案。因此,為了保持一致性,最好使用不帶 __name__ 檢查的最小的 __main__.py

另請參閱

請參閱 venv,瞭解標準庫中帶有最小 __main__.py 的包的示例。它不包含 if __name__ == '__main__' 程式碼塊。您可以使用 python -m venv [directory] 呼叫它。

有關直譯器可執行檔案的 -m 標誌的更多詳細資訊,請參閱 runpy

有關如何執行打包為 .zip 檔案的應用程式,請參閱 zipapp。在這種情況下,Python 會在存檔的根目錄中查詢 __main__.py 檔案。

import __main__

無論 Python 程式是從哪個模組啟動的,在同一程式中執行的其他模組都可以透過匯入 __main__ 模組來匯入頂層環境的範圍(名稱空間)。這不會匯入 __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__ 不會導致意外執行位於 start 模組的 if __name__ == "__main__" 程式碼塊中,旨在用於指令碼使用的頂層程式碼的問題。為什麼會這樣?

Python 在直譯器啟動時在 sys.modules 中插入一個空的 __main__ 模組,並透過執行頂層程式碼來填充它。在我們的示例中,這是逐行執行並匯入 namelystart 模組。反過來,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 的實現中。