__main__ --- 頂層程式碼環境


在 Python 中,特殊名稱 __main__ 用於兩個重要的建構:

  1. 程式頂層環境的名稱,可以使用 __name__ == '__main__' 運算式進行檢查;和

  2. 在 Python 套件中的 __main__.py 檔案。

這兩種機制都與 Python 模組有關;使用者如何與它們互動以及它們如何彼此互動。下面會詳細解釋它們。如果你不熟悉 Python 模組,請參閱教學章節 模組 (Module) 的介紹。

__name__ == '__main__'

當引入 Python 模組或套件時,__name__ 設定為模組的名稱。通常來說,這是 Python 檔案本身的名稱,且不含 .py 副檔名:

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

如果檔案是套件的一部分,則 __name__ 也會包含父套件 (parent package) 的路徑:

>>> 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 陳述式初始化時,使用常見的慣用語法 (idiom) 來有條件地執行程式碼:

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

也參考

若要更詳細地了解如何在所有情況下設定 __name__,請參閱教學章節 模組 (Module)

慣用 (Idiomatic) 用法

某些模組包含僅供腳本使用的程式碼,例如剖析命令列引數或從標準輸入取得資料。如果從不同的模組匯入這樣的模組(例如對其進行單元測試 (unit test)),則腳本程式碼也會無意間執行。

這就是使用 if __name__ == '__main__' 程式碼區塊派上用場的地方。除非該模組在頂層環境中執行,否則此區塊中的程式碼不會執行。

if __name__ == '__main__' 下面的區塊中放置盡可能少的陳述式可以提高程式碼的清晰度和正確性。大多數情況下,名為 main 的函式封裝 (encapsulate) 了程式的主要行為:

# 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 函式本身是隔離的 (isolated) 並且可以在其他地方引入。當引入 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 套件,請參閱 套件 (Package) 的教學章節。最常見的是,__main__.py 檔案用於為套件提供命令列介面。假設下面有虛構的套件 "bandclass":

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

當使用 -m 旗標 (flag) 直接從命令列呼叫套件本身時,將執行 __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 是相對引入的範例。在引用套件內的模組時,可以使用此引入樣式。有關更多詳細資訊,請參閱 模組 (Module) 教學章節中的 套件內引用

慣用 (Idiomatic) 用法

__main__.py 的內容通常不會被 if __name__ == '__main__' 區塊包圍。相反的,這些檔案保持簡短並引入其他模組的函式來執行。那些其他模組就可以輕鬆地進行單元測試並且可以正確地重複使用。

如果在套件裡面的 __main__.py 檔案使用 if __name__ == '__main__' 區塊,它依然會如預期般地運作。因為當引入套件,其 __name__ 屬性將會包含套件的路徑:

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

但這對於 .zip 檔案根目錄中的 __main__.py 檔案不起作用。因此,為了保持一致性,最小的 __main__.py 如下面提到的 venv 會是首選。

也參考

請參閱 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())

現在,如果我們啟動程式,結果將如下所示:

$ 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__ 模組,並透過執行頂層程式碼來填充它。在我們的範例中,這是 start 模組,它將逐行執行並引入 namely。接著,namely 引入 __main__(其實是 start)。這就是一個引入循環!幸運的是,由於部分填充的 __main__ 模組存在於 sys.modules 中,Python 將其傳遞給 namely。請參閱引入系統參考文件中的關於 __main__ 的特別考量了解其工作原理的詳細資訊。

Python REPL 是「頂層環境」的另一個範例,因此 REPL 中定義的任何內容都成為 作域的一部分:

>>> 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 的實作。