在指令行中使用 Python 時,它可以接收大約 20 個選項(option),文法格式如下:
python [-bBdEhiIOqsSuvVWx?] [-c command | -m module-name | script | - ] [args]
本文想要聊聊比較特殊的“-m”選項:關于它的典型用法、原了解析與發展演變的過程。
首先,讓我們用“--help”來看看它的解釋:
-m mod run library module as a script (terminates option list)
"mod"是“module”的縮寫,即“-m”選項後面的内容是 module(子產品),其作用是把子產品當成腳本來運作。
“terminates option list”意味着“-m”之後的其它選項不起作用,在這點上它跟“-c”是一樣的,都是“終極選項”。官方把它們定義為“接口選項”(Interface options),需要差別于其它的普通選項或通用選項。
-m 選項的五個典型用法
Python 中有很多使用 -m 選項的場景,相信大家可能會用到或者看見過,我在這裡想分享 5 個。
在 Python3 中,隻需一行指令就能實作一個簡單的 HTTP 服務:
python -m http.server 8000 # 注:在 Python2 中是這樣 python -m SimpleHTTPServer 8000
執行後,在本機打開“http://localhost:8000” ,或者在區域網路内的其它機器上打開“http://本機ip:8000” ,就能通路到執行目錄下的内容,例如下圖就是我本機的内容:
與此類似,我們隻需要一行指令“python -m pydoc -p xxx”,就能生成 HTML 格式的官方幫助文檔,可以在浏覽器中通路。
上面的指令執行了 pydoc 子產品,會在 9000 端口啟動一個 http 服務,在浏覽器中打開,我的結果如下:
它的第三個常見用法是執行 pdb 的調試指令“python -m pdb xxx.py”,以調試模式來執行“xxx.py”腳本:
第四個同樣挺有用的場景是用 timeit 在指令行中測試一小段代碼的運作時間。以下的 3 段代碼,用不同的方式拼接 “0-1-2-……-99” 數字串。可以直覺地看出它們的效率差異:
最後,還有一種常常被人忽略的場景:“python -m pip install xxx”。我們可能會習慣性地使用“pip install xxx”,或者做了版本區分時用“pip3 install xxx”,總之不在前面用“python -m”做指定。但這種寫法可能會出問題。
很巧合的是,在本月初(2019.11.01),Python 的核心開發者、第一屆指導委員會 五人成員之一的 Brett Cannon 專門寫了一篇部落格《Why you should use "python -m pip" 》,提出應該使用“python -m pip”的方式,并做了詳細的解釋。
他的主要觀點是:在存在多個 Python 版本的環境中,這種寫法可以精确地控制三方庫的安裝位置。例如用“python3.8 -m pip”,可以明确指定給 3.8 版本安裝,而不會混淆成其它的版本。
(延伸閱讀:關于 Brett 的文章,這有一篇簡短的歸納《原來我一直安裝 Python 庫的姿勢都不對呀!》)
-m 選項的兩種原了解析
看了前面的幾種典型用法,你是否開始好奇:“-m”是怎麼運作的?它是怎麼實作的?
對于“python -m name”,一句話解釋:Python 會檢索sys.path ,查找名字為“name”的子產品或者包(含命名空間包),并将其内容當成“__main__”子產品來執行。
1、對于普通子產品
以“.py”為字尾的檔案就是一個子產品,在“-m”之後使用時,隻需要使用子產品名,不需要寫出字尾,但前提是該子產品名是有效的,且不能是用 C 語言寫成的子產品。
在“-m”之後,如果是一個無效的子產品名,則會報錯“No module named xxx”。
如果是一個帶字尾的子產品,則首先會導入該子產品,然後可能報錯:Error while finding module specification for 'xxx.py' (AttributeError: module 'xxx' has no attribute '__path__'。
對于一個普通子產品,有時候這兩種寫法表面看起來是等效的:
兩種寫法都會把定位到的子產品腳本當成主程式入口來執行,即在執行時,該腳本的__name__ 都是”__main__“,跟 import 導入方式是不同的。
但它的前提是:在執行目錄中存在着“test.py”,且隻有唯一的“test”子產品。對于本例,如果換一個目錄執行的話,“python test.py”當然會報找不到檔案的錯誤,然而,“python -m test”卻不會報錯,因為解釋器在周遊sys.path 時可以找到同名的“test”子產品,并且執行:
由此差異,我們其實可以總結出“-m”的用法:已知一個子產品的名字,但不知道它的檔案路徑,那麼使用“-m”就意味着交給解釋器自行查找,若找到,則當成腳本執行。
以前文的“python -m http.server 8000”為例,我們也可以找到“server”子產品的絕對路徑,然後執行,盡管這樣會變得很麻煩。
那麼,“-m”方式與直接運作腳本相比,在實作上有什麼不同呢?
- 直接運作腳本時,相當于給出了腳本的完整路徑(不管是絕對路徑還是相對路徑),解釋器根據檔案系統的查找機制, 定位到該腳本,然後執行
- 使用“-m”方式時,解釋器需要在不 import 的情況下,在所有子產品命名空間 中查找,定位到腳本的路徑,然後執行。為了實作這個過程,解釋器會借助兩個子產品:pkgutil 和 runpy ,前者用來擷取所有的子產品清單,後者根據子產品名來定位并執行腳本
2、對于包内子產品
如果“-m”之後要執行的是一個包,那麼解釋器經過前面提到的查找過程,先定位到該包,然後會去執行它的“__main__”子子產品,也就是說,在包目錄下需要實作一個“__main__.py”檔案。
換句話說,假設有個包的名稱是“pname”,那麼,“python -m pname”,其實就等效于“python -m pname.__main__”。
仍以前文建立 HTTP 服務為例,“http”是 Python 内置的一個包,它沒有“__main__.py”檔案,是以使用“-m”方式執行時,就會報錯:No module named http.__main__; 'http' is a package and cannot be directly executed。
作為對比,我們可以看看前文提到的 pip,它也是一個包,為什麼“python -m pip”的方式可以使用呢?當然是因為它有“__main__.py”檔案:
“python -m pip”實際上執行的就是這個“__main__.py”檔案,它主要作為一個調用入口,調用了核心的"pip._internal.main"。
http 包因為沒有一個統一的入口子產品,是以采用了“python -m 包.子產品”的方式,而 pip 包因為有統一的入口子產品,是以加了一個“__main__.py”檔案,最後隻需要寫“python -m 包”,簡明直覺。
-m 選項的十年演變過程
最早引入 -m 選項的是 Python 2.4 版本(2004年),當時功能還挺受限,隻能作用于普通的内置子產品(如 pdb 和 profile)。
随後,知名開發者 Nick Coghlan 提出的《PEP 338 -- Executing modules as scripts》把它的功能提升了一個台階。這個 PEP 在 2004 年提出,最終實作在 2006 年的 2.5 版本。
(插個題外話:Nick Coghlan 是核心開發者中的核心之一,也是第一屆指導委員會的五人成員之一。記得當初看材料,他是在 2005 年被選為核心開發者的,這時間與 PEP-338 的時間緊密貼合)
這個 PEP 的幾個核心點是:
- 結合了 PEP-302 的新探針機制(new import hooks),提升了解釋器查找包内子產品的能力
- 結合了其它的導入機制(例如zipimport 和當機子產品(frozen modules)),拓展了解釋器查找子產品的範圍與精度
- 開發了新的runpy.run_module(modulename) 來實作本功能,而不用修改 CPython 解釋器,如此可友善移植到其它解釋器
至此,-m 選項使得 Python 可以在所有的命名空間内定位到指令行中給定的子產品。
2009 年,在 Python 3.1 版本中,隻需給定包的名稱,就能定位和運作它的“__main__”子子產品。2014 年,-m 擴充到支援命名空間包。
至此,經過十年的發展演變,-m 選項變得功能齊全,羽翼豐滿。
最後,我們來個 ending 吧:-m 選項可能看似不起眼,但它絕對是最特别的選項之一,它使得在指令行中,使用内置子產品、标準包與三方庫時變得更輕松便利。有機會就多用一下吧,體會它帶來的愉悅體驗。
------------------
公衆号【Python貓】, 本号連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦。