__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

參見

有關標準庫中帶有最小化 __main__.py 的包的示例,請參見 venv。它不包含 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`!')

    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

請注意,匯入 __main__ 並不會導致意外執行 start 模組的 if __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__ 作用域在 pdbrlcompleter 的實現中使用。