"__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" 時，"echo" 和 "main" 函式將被
定義，但它們都不會被呼叫，因為 "__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" 檔案不起作用。因此，為了
保持一致性，最小的、沒有 "__name__" 檢查的 "__main__.py" 會是首選。

也參考:

  請參閱 "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__" 作用域用於 "pdb" 和 "rlcompleter" 的實作。
