:point_up_2:  Python貓 ” ,一個值得加星標的 公衆號

原作: BRETT CANNON

譯者:豌豆花下貓@Python貓

英文: https://snarky.ca/the-many-ways-to-pass-code-to-python-from-the-terminal

爲了我們推出的 VS CodePython 插件[1],我寫了一個簡單的腳本來生成變更日誌[2](類似於 Towncrier [3],但簡單些,支持 Markdown ,符合我們的需求)。在發佈過程中,有一個步驟是運行 python news ,它會將 Python 指向我們代碼中的"news"目錄。

前幾天,一位合作者問這是如何工作的,似乎我們團隊中的每個人都知道如何使用 -m 請參閱我的有關帶 -m 使用 pip 的文章[4],瞭解原因。

這使我意識到其他人可能不知道有五花八門的方法可以將 Python 指向要執行的代碼,因此有了這篇文章。

1、通過標準輸入和管道

因爲如何用管道傳東西給一個進程是屬於 shell 的內容,我不打算深入解釋。毋庸置疑,你可以將代碼傳遞到 Python 中。

# 管道傳內容給 python
echo "print('hi')" | python

如果將文件重定向到 Python,這顯然也可以。

# 重定向一個文件給 python
python < spam.py

歸功於 Python 的 UNIX 傳統,這些都不太令人感到意外。

2、通過 -c 指定的字符串

如果你只需要快速地檢查某些內容,則可以在命令行中將代碼作爲字符串傳遞。

# 使用 python 的 -c 參數
python -c "print('hi')"

當需要檢查僅一行或兩行代碼時,我個人會使用它,而不是啓動 REPL (譯註:Read Eval Print Loop,即交互式解釋器,例如在 windows 控制檯中輸入 python , 就會進入交互式解釋器。-c 參數用法可以省去進入解釋器界面的過程)

3、文件的路徑

最衆所周知的傳代碼給 python 的方法很可能是通過文件路徑。

# 指定 python 的文件路徑
python spam.py

要實現這一點的關鍵是將包含該文件的目錄放到 sys.path 裏。這樣你的所有導入都可以繼續使用。但這也是爲什麼你不能/不應該傳入包含在一個包裏的模塊路徑。因爲 sys.path 可能不包含該包的目錄,因此所有的導入將相對於與你預期的包不同的目錄。

4、對包使用 -m

執行 Python 包的正確方法是使用 -m 並指定要運行的包名。

python -m spam

它在底層使用了runpy[5]。要在你的項目中做到這點,只需要在包裏指定一個 __main__.py 文件,它將被當成 __main__ 執行。而且子模塊可以像任何其它模塊一樣導入,因此你可以對其進行各種測試。

我知道有些人喜歡在一個包裏寫一個 main 子模塊,然後將其 __main__.py 寫成:

from . import main

if __name__ == "__main__":
    main.main()

就我個人而言,我不感冒於單獨的 main 模塊,而是直接將所有相關的代碼放入 __main__.py ,因爲我感覺這些模塊名是多餘的。

(譯註:即作者不關心作爲入口文件的"main"或者“__main__”模塊,因爲執行時只需用它們的包名即可。我認爲這也暗示了入口模塊不該再被其它模塊 import。我 上篇文章 [6]比作者的觀點激進,認爲連那句 if 語句都不該寫。)

5、目錄

定義 __main__.py 也可以擴展到目錄。如果你看一下促成此博客文章的示例, python news 可執行,就是因爲 news 目錄有一個 __main__.py 文件。該目錄就像一個文件路徑被 Python 執行了。

現在你可能會問:“爲什麼不直接指定文件路徑呢?”好吧,坦白說,關於文件路徑,有件事得說清楚。:smile:在發佈過程中,我可以簡單地寫上說明,讓運行 python news/announce.py ,但是並沒有確切的理由說明這種機制何時存在。

再加上我以後可以更改文件名,而且沒人會注意到。再加上我知道代碼會帶有輔助文件,因此將其放在目錄中而不是單獨作爲單個文件是有意義的。

當然,我也可以將它變爲一個使用 -m 的包,但是沒必要,因爲 announce 腳本很簡單,我知道它要保持成爲一個單獨的自足的文件(少於 200 行,並且測試模塊也大約是相同的長度)

況且, __main__.py 文件非常簡單。

import runpy
# Change 'announce' to whatever module you want to run.
runpy.run_module('announce', run_name='__main__', alter_sys=True)

現在顯然必須要處理依賴關係,但是如果你的腳本僅使用標準庫或將依賴模塊放在 __main__.py 旁邊 (譯註:即同級目錄) ,那麼就足夠了!

(譯註:我覺得作者在此有點“炫技”了,因爲這種寫法的前提是得知道 runpy 的用法,但是就像前一條所寫的用 -m 參數運行一個包,在底層也是用了 runpy。不過炫技的好處也非常明顯,即 __main__.py 裏不用導入 announce 模塊,還是以它爲主模塊執行,也就不會破壞原來的依賴導入關係)

6、執行一個壓縮文件

如果你確實有多個文件和/或依賴模塊,並且希望將所有代碼作爲一個單元發佈,你可以用一個 __main__.py ,放置在一個壓縮文件中,並把壓縮文件所在目錄放在 sys.path 裏,Python 會替你運行 __main__.py 文件。

# 將一個壓縮包傳給 Python
python app.pyz

人們現在習慣上用 .pyz 文件擴展名來命名此類壓縮文件,但這純粹是傳統,不會影響任何東西;你當然也可以用 .zip 文件擴展名。

爲了簡化創建此類可執行的壓縮文件,標準庫提供了zipapp[7]模塊。它會爲你生成 __main__.py 並添加一條組織行( shebang line ),因此你甚至不需要指定 python,如果你不想在 UNIX 上指定它的話。如果你想移動一堆純 Python 代碼,這是一種不錯的方法。

不幸的是,僅當壓縮文件包含的所有代碼都是純 Python 時,才能這樣運行壓縮文件。執行壓縮文件對擴展模塊無效(這就是爲什麼 setuptools 有一個 zip_safe[8]標誌的原因)。 (譯註:擴展模塊 extension module,即 C/C++ 之類的非 Python 文件)

要加載擴展模塊,Python 必須調用 dlopen()[9]函數,它要傳入一個文件路徑,但當該文件路徑就包含在壓縮文件內時,這顯然不起作用。

我知道至少有一個人與 glibc 團隊交談過,關於支持將內存緩衝區傳入壓縮文件,以便 Python 可以將擴展模塊讀入內存,並將其傳給壓縮文件,但是如果內存爲此服務,glibc 團隊並不同意。

但是,並非所有希望都喪失了!你可以使用諸如shiv[10]之類的項目,它會捆綁(bundle)你的代碼,然後提供一個 __main__.py 來處理壓縮文件的提取、緩存,然後爲你執行代碼。儘管不如純 Python 解決方案理想,但它確實可行,並且在這種情況下算得上是優雅的。

(譯註:翻譯水平有限,難免偏差。我加註了部分內容,希望有助於閱讀。請搜索關注“ Python貓 ”,閱讀更多優質的原創或譯作。)

參考鏈接

[0] https://snarky.ca/the-many-ways-to-pass-code-to-python-from-the-terminal/

[1] https://marketplace.visualstudio.com/items?itemName=ms-python.python

[2] https://github.com/microsoft/vscode-python/tree/master/news

[3] https://pypi.org/project/towncrier

[4] https://snarky.ca/why-you-should-use-python-m-pip

[5] https://docs.python.org/3/library/runpy.html#module-runpy

[6] https://mp.weixin.qq.com/s/1ehySR5NH2v1U8WIlXflEQ

[7] https://docs.python.org/3/library/zipapp.html#module-zipapp

[8] https://setuptools.readthedocs.io/en/latest/setuptools.html#setting-the-zip-safe-flag

[9] https://linux.die.net/man/3/dlopen

[10] https://pypi.org/project/shiv

期待更多原創好文

相關文章