參觀 Bytecode 工廠
雖然 Python 在分類上算是被分類在直譯式程式語言,但 Python 程式碼在執行之前,會先被編譯成 Bytecode,這個在前面幾章介紹過好幾次。而 .pyc
檔,就是經過編譯之後的 Bytecode 所產生的檔案,產生 .pyc
檔案最主要的目的是為了提高程式的執行效率。這個章節我們來看看這些 .pyc
到底是怎麼生成的,裡面又包了哪些有趣的東西。
只要有 .pyc
就行了
為了做實驗,我先準備一個很厲害(並沒有)的 hello
模組:
def greeting(name):
print(f"Hello, {name}!")
以及主程式 app.py
:
from hello import greeting
greeting("Kitty")
其實這些程式碼沒什麼營養,就只是展示用途而已。執行 python app.py
指令之後,應該會發現在目錄下多了一個 __pycache__
的資料夾,裡面有一個 hello.cpython-312.pyc
的檔案
├── __pycache__
│ └── hello.cpython-312.pyc
├── app.py
└── hello.py
hello.cpython-312.pyc
檔名的編碼方式也不難猜,就是看用什麼版本的 Python 來編譯的。
這個檔案就是 Python 編譯過後的 Bytecode 檔案。不過 這裡會看到只有產生 hello.py
的 .pyc
檔案,如果 app.py
也要順便產生一份的話,可以利用內建模組 py_compile
來達成:
$ python -m py_compile app.py
或是另一個更方便的模組 compileall
:
$ python -m compileall .
一次把所有的 .py
檔案都編譯成 .pyc
檔案。
接著可以把這些 .py
檔刪掉,進到 __pycache__
目錄裡,把 app.cpython-312.pyc
跟 hello.cpython-312.pyc
分別改成 app.pyc
以及 hello.pyc
,然後執行 python app.pyc
指令,就會發現程式還是可以正常執行的。
$ python app.pyc
Hello, Kitty!
不過這沒什麼神秘的,在上個章節我們就有提到 Python 在讀取程式檔案的過程中,有個 maybe_pyc_file()
就會檢查是不是 .pyc
檔,是的話會用二進位的方式把檔案讀進來執行。
所以有些時候因為某些原因,不想給出 .py
原始碼,光是提供 .pyc
也是可以執行的。至於為什麼不想提供原始碼就不是我關心的話題,我比較關心的是為什麼執行 python app.py
的時候,主程式 app.py
不會產生 .pyc
,但在主程式裡被 import
進來的 hello
模組就會產生。
這得從 Python 的 import 機制來看...
「可能是」.pyc 檔?
CPython 的 import 機制在前面的章節曾經看過,前半段的工作在 Python/import.c
,後半段則交由 Lib/importlib/_bootstrap.py
處理。在
但在執行主程式的時候,在上個章節也看到從最一開始的 Py_BytesMain()
追到最後執行的 run_eval_code_obj()
函數,都沒有看到有產生 .pyc
檔的行為。雖然在執行的時候,Python 會先檢查看看有沒有對應的 .pyc
檔,如果有就會把 .pyc
以二進位的方式把檔案讀進來,不然就是執行 pyrun_file()
函數。而大部份時候程式只會被執行一次,所以雖然特地將它再轉存成 .pyc
檔案來提高下一回的執行效率也不是不行,但看起來意義不大,所以 Python 在執行主程式的時候,並不會特地產生 .pyc
檔。相對的其它被 import
進來的模組,可能會在不同的程式中被重複使用,所以將它們轉成 .pyc
來提高後續的執行效率就有意義了。
這個 maybe_pyc_file()
函數怎麼會這麼沒有自信,程式的東西大多是非黑即白、非 0 則 1 嗎?怎麼會有什麼「可能是」?我們來看看是怎麼回事:
static int
maybe_pyc_file(FILE *fp, PyObject *filename, int closeit)
{
PyObject *ext = PyUnicode_FromString(".pyc");
if (ext == NULL) {
return -1;
}
Py_ssize_t endswith = PyUnicode_Tailmatch(filename, ext, 0, PY_SSIZE_T_MAX, +1);
Py_DECREF(ext);
if (endswith) {
return 1;
}
// ... 略 ...
/* Read only two bytes of the magic. If the file was opened in
text mode, the bytes 3 and 4 of the magic (\r\n) might not
be read as they are on disk. */
unsigned int halfmagic = PyImport_GetMagicNumber() & 0xFFFF;
unsigned char buf[2];
/* Mess: In case of -x, the stream is NOT at its start now,
and ungetc() was used to push back the first newline,
which makes the current stream position formally undefined,
and a x-platform nightmare.
Unfortunately, we have no direct way to know whether -x
was specified. So we use a terrible hack: if the current
stream position is not 0, we assume -x was specified, and
give up. Bug 132850 on SourceForge spells out the
hopelessness of trying anything else (fseek and ftell
don't work predictably x-platform for text-mode files).
*/
int ispyc = 0;
if (ftell(fp) == 0) {
if (fread(buf, 1, 2, fp) == 2 &&
((unsigned int)buf[1]<<8 | buf[0]) == halfmagic)
ispyc = 1;
rewind(fp);
}
return ispyc;
}
它會檢查:
- 附檔名是不是
.pyc
。 - 如果不是
.pyc
附檔名,則檢查檔案的開頭兩個位元組是不是 Python 的「魔術數字」。
這魔術數字我們待會再看,但在中間有一段註解寫到有個複雜的判斷,細節不明,詳細情況可能得再去追 SourceForge 上的 Bug 清單,但看起來是有跨平台編譯出來不容易判斷的問題,在遇到某些參數的時候會直接放棄判斷,我想這也是這個函數會用 maybe_
來命名的原因。
魔術數字,Magic!
那麼這個「魔術數字」是什麼呢?來看看 PyImport_GetMagicNumber()
是怎麼拿這個號碼的:
long
PyImport_GetMagicNumber(void)
{
long res;
PyInterpreterState *interp = _PyInterpreterState_GET();
PyObject *external, *pyc_magic;
external = PyObject_GetAttrString(IMPORTLIB(interp), "_bootstrap_external");
if (external == NULL)
return -1;
pyc_magic = PyObject_GetAttrString(external, "_RAW_MAGIC_NUMBER");
Py_DECREF(external);
if (pyc_magic == NULL)
return -1;
res = PyLong_AsLong(pyc_magic);
Py_DECREF(pyc_magic);
return res;
}
看起來是開始借用 Python 的 importlib._bootstrap_external
模組來取得 _RAW_MAGIC_NUMBER
這個屬性,繼續追看看是怎麼回事:
MAGIC_NUMBER = (3531).to_bytes(2, 'little') + b'\r\n'
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
嘿嘿,又是親切的 Python 程式碼了,這個 .to_bytes()
方法的第二個參數,表示要用 little
,也就是 little-endian
的方式來呈現,除了 little
還有另一種 big-endian
,呈現的效果不太一樣。我用數字 9527 來算一下:
9527
這個數字的十六進位是0x2537
。- 在
big-endian
的呈現方式,會把「最高有效位元組(Most Significant Byte, MSB)」放在最前面,以結果來說就是\x25\x37
- 而在
little-endian
的呈現方式,會把「最低有效位元組(Least Significant Byte, LSB)」放在最前面,也就是\x37\x25
回到原本的程式,看的出來是把數字 3531
數字轉成 2 個 Byte,然後再加上一個 \r\n
,這就是 Python 的「魔術數字」了。如果再往上翻一點,會看到這個 3521
是怎麼來的:
# Known values:
# Python 1.5: 20121
# Python 1.5.1: 20121
# Python 1.5.2: 20121
# Python 1.6: 50428
# Python 2.0: 50823
# ... 略 ...
# Python 2.7a0 62211 (introduce MAP_ADD and SET_ADD)
# Python 3000: 3000
# 3010 (removed UNARY_CONVERT)
# 3020 (added BUILD_SET)
# ... 略 ...
# Python 3.12b1 3530 (Shrink the LOAD_SUPER_ATTR caches)
# Python 3.12b1 3531 (Add PEP 695 changes)
#
# Python 3.13 will start with 3550