__main__ --- 頂層程式碼環境


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

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

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

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

__name__=='__main__'

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

>>>importconfigparser>>>configparser.__name__'configparser'

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

>>>fromconcurrent.futuresimportprocess>>>process.__name__'concurrent.futures.process'

但是,如果模組在頂層程式碼環境中執行,則其__name__ 將被設定為字串'__main__'

什麼是「頂層程式碼環境」?

__main__ 是執行頂層程式碼的環境名稱。「頂層程式碼」是使用者指定且第一個開始運作的 Python 模組。它是「頂層」的原因是因為它引入程式所需的所有其他模組。有時「頂層程式碼」被稱為應用程式的入口點

頂層程式碼環境可以是:

  • 互動式提示字元的作用域:

    >>>__name__'__main__'
  • 將 Python 模組作為檔案引數傳遞給 Python 直譯器:

    $pythonhelloworld.pyHello, world!
  • 使用-m 引數傳遞給 Python 直譯器的 Python 模組或套件:

    $python-mtarfileusage: tarfile.py [-h] [-v] (...)
  • Python 直譯器從標準輸入讀取 Python 程式碼:

    $echo"import this"|pythonThe Zen of Python, by Tim PetersBeautiful is better than ugly.Explicit is better than implicit....
  • 使用-c 引數傳遞給 Python 直譯器的 Python 程式碼:

    $python-c"import this"The Zen of Python, by Tim PetersBeautiful 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.pyimportshleximportsysdefecho(phrase:str)->None:"""A dummy wrapper around print."""# for demonstration purposes, you can imagine that there is some# valuable and reusable logic inside this functionprint(phrase)defmain()->int:"""Echo the input arguments to standard output"""phrase=shlex.join(sys.argv)echo(phrase)return0if__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 陳述式,則相當於回傳此值)。

透過我們自己主動遵循這個慣例,我們的模組在直接執行時(即pythonecho.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-mbandclass

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

# bandclass/__main__.pyimportsysfrom.studentimportsearch_studentsstudent_name=sys.argv[1]iflen(sys.argv)>=2else''print(f'Found student:{search_students(student_name)}')

請注意,from.studentimportsearch_students 是相對引入的範例。在引用套件內的模組時,可以使用此引入樣式。有關更多詳細資訊,請參閱模組 (Module) 教學章節中的套件內引用

慣用 (Idiomatic) 用法

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

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

>>>importasyncio.__main__>>>asyncio.__main__.__name__'asyncio.__main__'

但這對於.zip 檔案根目錄中的__main__.py 檔案不起作用。因此,為了保持一致性,最小的、沒有__name__ 檢查的__main__.py 會是首選。

也參考

請參閱venv 作為標準函式庫中具有最小__main__.py 的套件為範例。它不包含if__name__=='__main__' 區塊。你可以使用python-mvenv[directory] 來呼叫它。

請參閱runpy 取得有關直譯器可執行檔的-m 旗標的更多詳細資訊。

請參閱zipapp 了解如何執行打包成.zip 檔案的應用程式。在這種情況下,Python 會在封存檔案的根目錄中尋找__main__.py 檔案。

import__main__

無論 Python 程式是從哪個模組啟動的,在同一程式中執行的其他模組都可以透過匯入__main__ 模組來引入頂層環境的作用域 (namespace)。這不會引入__main__.py 檔案,而是引入接收特殊名稱'__main__' 的模組。

這是一個使用__main__ 命名空間的範例模組:

# namely.pyimport__main__defdid_user_define_their_name():return'my_name'indir(__main__)defprint_user_name():ifnotdid_user_define_their_name():raiseValueError('Define the variable `my_name`!')if'__file__'indir(__main__):print(__main__.my_name,"found in file",__main__.__file__)else:print(__main__.my_name)

該模組的範例用法如下:

# start.pyimportsysfromnamelyimportprint_user_name# my_name = "Dinsdale"defmain():try:print_user_name()exceptValueErrorasve:returnstr(ve)if__name__=="__main__":sys.exit(main())

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

$pythonstart.pyDefine the variable `my_name`!

程式的結束代碼將為 1,表示出現錯誤。取消註解my_name="Dinsdale" 而修復程式後,現在它以狀態碼 0 結束,表示成功:

$pythonstart.pyDinsdale 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 中定義的任何內容都成為 作域的一部分:

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