zipapp —— 管理可執行的 Python zip 打包文件?

3.5 新版功能.

源代碼: Lib/zipapp.py


本模塊提供了一套管理工具,用于創建包含 Python 代碼的壓縮文件,這些文件可以 直接由 Python 解釋器執行。 本模塊提供 命令行界面Python API

簡單示例?

下述例子展示了用 命令行界面 根據含有 Python 代碼的目錄創建一個可執行的打包文件。 運行后該打包文件時,將會執行 myapp 模塊中的 main 函數。

$ python -m zipapp myapp -m "myapp:main"
$ python myapp.pyz
<output from myapp>

命令行界面?

若要從命令行調用,則采用以下形式:

$ python -m zipapp source [options]

如果 source 是個目錄,將根據 source 的內容創建一個打包文件。如果 source 是個文件,則應為一個打包文件,將會復制到目標打包文件中(如果指定了 -info 選項,將會顯示 shebang 行的內容)。

可以接受以下參數:

-o <output>, --output=<output>?

將程序的輸出寫入名為 output 的文件中。若未指定此參數,輸出的文件名將與輸入的 source 相同,并添加擴展名 .pyz。如果顯式給出了文件名,將會原樣使用(因此必要時應包含擴展名 .pyz)。

如果 source 是個打包文件,必須指定一個輸出文件名(這時 output 必須與 source 不同)。

-p <interpreter>, --python=<interpreter>?

給打包文件加入 #! 行,以便指定 解釋器 作為運行的命令行。另外,還讓打包文件在 POSIX 平臺上可執行。默認不會寫入 #! 行,也不讓文件可執行。

-m <mainfn>, --main=<mainfn>?

在打包文件中寫入一個 __main__.py 文件,用于執行 mainfnmainfn 參數的形式應為 “pkg.mod:fn”,其中 “pkg.mod”是打包文件中的某個包/模塊,“fn”是該模塊中的一個可調用對象。__main__.py 文件將會執行該可調用對象。

在復制打包文件時,不能設置 --main 參數。

-c, --compress?

利用 deflate 方法壓縮文件,減少輸出文件的大小。默認情況下,打包文件中的文件是不壓縮的。

在復制打包文件時,--compress 無效。

3.7 新版功能.

--info?

顯示嵌入在打包文件中的解釋器程序,以便診斷問題。這時會忽略其他所有參數,SOURCE 必須是個打包文件,而不是目錄。

-h, --help?

打印簡短的用法信息并退出。

Python API?

該模塊定義了兩個快捷函數:

zipapp.create_archive(source, target=None, interpreter=None, main=None, filter=None, compressed=False)?

source 創建一個應用程序打包文件。source 可以是以下形式之一:

  • 一個目錄名,或指向目錄的 path-like object ,這時將根據目錄內容新建一個應用程序打包文件。

  • 一個已存在的應用程序打包文件名,或指向這類文件的 path-like object,這時會將該文件復制為目標文件(會稍作修改以反映出 interpreter 參數的值)。必要時文件名中應包括 .pyz 擴展名。

  • 一個以字節串模式打開的文件對象。該文件的內容應為應用程序打包文件,且假定文件對象定位于打包文件的初始位置。

target 參數定義了打包文件的寫入位置:

  • 若是個文件名,或是 path-like object,打包文件將寫入該文件中。

  • 若是個打開的文件對象,打包文件將寫入該對象,該文件對象必須在字節串寫入模式下打開。

  • 如果省略了 target (或為 None),則 source 必須為一個目錄,target 將是與 source 同名的文件,并加上 .pyz 擴展名。

參數 interpreter 指定了 Python 解釋器程序名,用于執行打包文件。這將以 “釋伴(shebang)”行的形式寫入打包文件的頭部。在 POSIX 平臺上,操作系統會進行解釋,而在 Windows 平臺則會由 Python 啟動器進行處理。省略 interpreter 參數則不會寫入釋伴行。如果指定了解釋器,且目標為文件名,則會設置目標文件的可執行屬性位。

參數 main 指定某個可調用程序的名稱,用作打包文件的主程序。僅當 source 為目錄且不含 __main__.py 文件時,才能指定該參數。main 參數應采用 “pkg.module:callable”的形式,通過導入“pkg.module”并不帶參數地執行給出的可調用對象,即可執行打包文件。如果 source 是目錄且不含``__main__.py`` 文件,省略 main 將會出錯,生成的打包文件將無法執行。

可選參數 filter 指定了回調函數,將傳給代表被添加文件路徑的 Path 對象(相對于源目錄)。在文件添加完成后,應返回 True

可選參數 compressed 指定是否要壓縮打包文件。若設為 True,則打包中的文件將用 deflate 方法進行壓縮;否則就不會壓縮。本參數在復制現有打包文件時無效。

sourcetarget 指定的是文件對象,則調用者有責任在調用 create_archive 之后關閉文件。

當復制已有的打包文件時,提供的文件對象只需 readreadline 方法,或 write 方法。當由目錄創建打包文件時,若目標為文件對象,將會將其傳給 類,且必須提供 zipfile.ZipFile 類所需的方法。

3.7 新版功能: 加入了 filtercompressed 參數。

zipapp.get_interpreter(archive)?

返回打包文件開頭的 行指定的解釋器程序。如果沒有 #! 行,則返回 None。參數 archive 可為文件名或在字節串模式下打開以供讀取的文件類對象。#! 行假定是在打包文件的開頭。

例子?

將目錄打包成一個文件并運行它。

$ python -m zipapp myapp
$ python myapp.pyz
<output from myapp>

同樣還可用 create_archive() 函數完成:

>>> import zipapp
>>> zipapp.create_archive('myapp', 'myapp.pyz')

要讓應用程序能在 POSIX 平臺上直接執行,需要指定所用的解釋器。

$ python -m zipapp myapp -p "/usr/bin/env python"
$ ./myapp.pyz
<output from myapp>

若要替換已有打包文件中的釋伴行,請用 create_archive() 函數另建一個修改好的打包文件:

>>> import zipapp
>>> zipapp.create_archive('old_archive.pyz', 'new_archive.pyz', '/usr/bin/python3')

若要原地更新打包文件,可用 BytesIO 對象在內存中進行替換,然后再覆蓋源文件。注意,原地覆蓋文件會有風險,出錯時會丟失原文件。這里沒有考慮出錯情況,但生產代碼則應進行處理。另外,這種方案僅當內存足以容納打包文件時才有意義:

>>> import zipapp
>>> import io
>>> temp = io.BytesIO()
>>> zipapp.create_archive('myapp.pyz', temp, '/usr/bin/python2')
>>> with open('myapp.pyz', 'wb') as f:
>>>     f.write(temp.getvalue())

指定解釋器程序?

注意,如果指定了解釋器程序再發布應用程序打包文件,需要確保所用到的解釋器是可移植的。Windows 的 Python 啟動器支持大多數常見的 POSIX #! 行,但還需要考慮一些其他問題。

  • 如果采用“/usr/bin/env python”(或其他格式的 python 調用命令,比如“/usr/bin/python”),需要考慮默認版本既可能是 Python 2 又可能是 Python 3,應讓代碼在兩個版本下均能正常運行。

  • 如果用到的 Python 版本明確,如“/usr/bin/env python3”,則沒有該版本的用戶將無法運行應用程序。(如果代碼不兼容 Python 2,可能正該如此)。

  • 因為無法指定“python X.Y以上版本”,所以應小心“/usr/bin/env python3.4”這種精確版本的指定方式,因為對于 Python 3.5 的用戶就得修改釋伴行,比如:

通常應該用“/usr/bin/env python2”或“/usr/bin/env python3”的格式,具體根據代碼適用于 Python 2 還是 3 而定。

用 zipapp 創建獨立運行的應用程序?

利用 zipapp 模塊可以創建獨立運行的 Python 程序,以便向最終用戶發布,僅需在系統中裝有合適版本的 Python 即可運行。操作的關鍵就是把應用程序代碼和所有依賴項一起放入打包文件中。

創建獨立運行打包文件的步驟如下:

  1. 照常在某個目錄中創建應用程序,于是會有一個 myapp 目錄,里面有個``__main__.py`` 文件,以及所有支持性代碼。

  2. 用 pip 將應用程序的所有依賴項裝入 myapp 目錄。

    $ python -m pip install -r requirements.txt --target myapp
    

    (這里假定在 requirements.txt 文件中列出了項目所需的依賴項,也可以在 pip 命令行中列出依賴項)。

  3. pip 在 myapp 中創建的 .dist-info 目錄,是可以刪除的。這些目錄保存了 pip 用于管理包的元數據,由于接下來不會再用到 pip,所以不是必須存在,當然留下來也不會有什么壞處。

  4. 用以下命令打包:

    $ python -m zipapp -p "interpreter" myapp
    

這會生成一個獨立的可執行文件,可在任何裝有合適解釋器的機器上運行。詳情參見 指定解釋器程序。可以單個文件的形式分發給用戶。

在 Unix 系統中,myapp.pyz 文件將以原有文件名執行。如果喜歡 “普通”的命令名,可以重命名該文件,去掉擴展名 .pyz 。在 Windows 系統中,myapp.pyz[w] 是可執行文件,因為 Python 解釋器在安裝時注冊了擴展名``.pyz`` 和 .pyzw

制作 Windows 可執行文件?

在 Windows 系統中,可能沒有注冊擴展名 .pyz,另外有些場合無法“透明”地識別已注冊的擴展(最簡單的例子是,subprocess.run(['myapp']) 就找不到——需要明確指定擴展名)。

因此,在 Windows 系統中,通常最好 由zipapp 創建一個可執行文件。雖然需要用到 C 編譯器,但還是相對容易做到的。基本做法有賴于以下事實,即 zip 文件內可預置任意數據,Windows 的 exe 文件也可以附帶任意數據。因此,創建一個合適的啟動程序并將 .pyz 文件附在后面,最后就能得到一個單文件的可執行文件,可運行 Python 應用程序。

合適的啟動程序可以簡單如下:

#define Py_LIMITED_API 1
#include "Python.h"

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#ifdef WINDOWS
int WINAPI wWinMain(
    HINSTANCE hInstance,      /* handle to current instance */
    HINSTANCE hPrevInstance,  /* handle to previous instance */
    LPWSTR lpCmdLine,         /* pointer to command line */
    int nCmdShow              /* show state of window */
)
#else
int wmain()
#endif
{
    wchar_t **myargv = _alloca((__argc + 1) * sizeof(wchar_t*));
    myargv[0] = __wargv[0];
    memcpy(myargv + 1, __wargv, __argc * sizeof(wchar_t *));
    return Py_Main(__argc+1, myargv);
}

若已定義了預處理器符號 WINDOWS,上述代碼將會生成一個 GUI 可執行文件。若未定義則生成一個可執行的控制臺文件。

直接使用標準的 MSVC 命令行工具,或利用 distutils 知道如何編譯 Python 源代碼,即可編譯可執行文件:

>>> from distutils.ccompiler import new_compiler
>>> import distutils.sysconfig
>>> import sys
>>> import os
>>> from pathlib import Path

>>> def compile(src):
>>>     src = Path(src)
>>>     cc = new_compiler()
>>>     exe = src.stem
>>>     cc.add_include_dir(distutils.sysconfig.get_python_inc())
>>>     cc.add_library_dir(os.path.join(sys.base_exec_prefix, 'libs'))
>>>     # First the CLI executable
>>>     objs = cc.compile([str(src)])
>>>     cc.link_executable(objs, exe)
>>>     # Now the GUI executable
>>>     cc.define_macro('WINDOWS')
>>>     objs = cc.compile([str(src)])
>>>     cc.link_executable(objs, exe + 'w')

>>> if __name__ == "__main__":
>>>     compile("zastub.c")

生成的啟動程序用到了 “受限 ABI”,所以可在任意版本的 Python 3.x 中運行。只要用戶的 PATH 中包含了 Python(python3.dll)路徑即可。

若要得到完全獨立運行的發行版程序,可將附有應用程序的啟動程序,與“內嵌版” Python 打包在一起即可。這樣在架構匹配(32位或64位)的任一 PC 上都能運行。

注意事項?

要將應用程序打包為單個文件,存在一些限制。大多數情況下,無需對應用程序進行重大修改即可解決。

  1. 如果應用程序依賴某個帶有 C 擴展的包,則此程序包無法由打包文件運行(這是操作系統的限制,因為可執行代碼必須存在于文件系統中,操作系統才能加載)。這時可去除打包文件中的依賴關系,然后要求用戶事先安裝好該程序包,或者與打包文件一起發布并在 __main__.py 中增加代碼,將未打包模塊的目錄加入 sys.path 中。采用增加代碼方式時,一定要為目標架構提供合適的二進制文件(可能還需在運行時根據用戶的機器選擇正確的版本加入 sys.path)。

  2. 若要如上所述發布一個 Windows 可執行文件,就得確保用戶在 PATH 中包含``python3.dll`` 的路徑(安裝程序默認不會如此),或者應把應用程序與內嵌版 Python 一起打包。

  3. 上述給出的啟動程序采用了 Python 嵌入 API。 這意味著應用程序將會是 sys.executable ,而*不是*傳統的 Python 解釋器。代碼及依賴項需做好準備。例如,如果應用程序用到了 multiprocessing 模塊,就需要調用 multiprocessing.set_executable() 來讓模塊知道標準 Python 解釋器的位置。

Python 打包應用程序的格式?

自 2.6 版開始,Python 即能夠執行包含 文件的打包文件了。為了能被 Python 執行,應用程序的打包文件必須為包含 __main__.py 文件的標準 zip 文件,__main__.py 文件將作為應用程序的入口運行。類似于常規的 Python 腳本,父級(這里指打包文件)將放入 sys.path ,因此可從打包文件中導入更多的模塊。

zip 文件格式允許在文件中預置任意數據。利用這種能力,zip 應用程序格式在文件中預置了一個標準的 POSIX “釋伴”行(#!/path/to/interpreter)。

因此,Python zip 應用程序的格式會如下所示:

  1. 可選的釋伴行,包含字符 b'#!',后面是解釋器名,然后是換行符 (b'\n')。 解釋器名可為操作系統 “釋伴”處理所能接受的任意值,或為 Windows 系統中的 Python 啟動程序。解釋器名在 Windows 中應用 UTF-8 編碼,在 POSIX 中則用 sys.getfilesystemencoding()

  2. 標準的打包文件由 zipfile 模塊生成。其中 必須 包含一個名為``__main__.py`` 的文件(必須位于打包文件的“根”目錄——不能位于某個子目錄中)。打包文件中的數據可以是壓縮或未壓縮的。

如果應用程序的打包文件帶有釋伴行,則在 POSIX 系統中可能需要啟用可執行屬性,以允許直接執行。

不一定非要用本模塊中的工具創建應用程序打包文件,本模塊只是提供了便捷方案,上述格式的打包文件可用任何方式創建,均可被 Python 接受。