楔子
上一章中,我們通過_PyEval_EvalFrameDefault看到了Python虛擬機的整體框架,那么這一章我們將深入到_PyEval_EvalFrameDefault的各個細節當中,深入剖析Python的虛擬機,在本章中我們將剖析Python虛擬機是如何完成對一般表達式的執行的。這里的一般表達式包括最基本的對象創建語句、打印語句等等。至於if、while等表達式,我們將其歸類於控制流語句,對於Python中控制流的剖析,我們將留到下一章。
簡單回顧
這里我們通過問與答的方式,簡單回顧一下前面的內容。
請問 Python 程序是怎么運行的?是編譯成機器碼后在執行的嗎?
不少初學者對 Python 存在誤解,以為它是類似 Shell 的解釋性腳本語言,其實並不是。雖然執行 Python 程序的 稱為 Python 解釋器,但它其實包含一個 "編譯器" 和一個 "虛擬機"。
當我們在命令行敲下 python xxxx.py
時,python 解釋器中的編譯器首先登場,將 Python 代碼編譯成 PyCodeObject 對象。PyCodeObject 對象包含 字節碼 以及執行字節碼所需的 名字 以及 常量。
當編譯器完成編譯動作后,接力棒便傳給 虛擬機。虛擬機 維護執行上下文,逐行執行 字節碼 指令。執行上下文中最核心的 名字空間,便是由 虛擬機 維護的。
因此,Python 程序的執行原理其實更像 Java,可以用兩個詞來概括—— 虛擬機和字節碼。不同的是,Java 編譯器 javac 與 虛擬機 java 是分離的,而 Python 將兩者整合成一個 python 命令。
pyc 文件保存什么東西,有什么作用?
Python 程序執行時需要先由 編譯器 編譯成 PyCodeObject 對象,然后再交由 虛擬機 來執行。不管程序執行多少次,只要源碼沒有變化,編譯后得到的 PyCodeObject 對象就肯定是一樣的。因此,Python 將 PyCodeObject 對象序列化並保存到 pyc 文件中。當程序再次執行時,Python 直接從 pyc 文件中加載代碼對象,省去編譯環節。當然了,當 py 源碼文件改動后,pyc 文件便失效了,這時 Python 必須重新編譯 py 文件。
如何查看 Python 程序的字節碼?
Python 標准庫中的 dis 模塊,可以對 PyCodeObject 對象 以及 函數進行反編譯,並顯示其中的 字節碼。
其實dis.dis最終反編譯的就是字節碼,只不過我們可以傳入一個函數,會自動獲取其字節碼。比如:函數foo,我們可以dis.dis(foo)、dis.dis(foo.__code__)、dis.dis(foo.__code__.co_code),最終都是對字節碼進行反編譯的。
在這里我們說幾個常見的字節碼指令,因為它太常見了以至於我們這里必須要提一下,然后再舉例說明。
LOAD_CONST: 加載一個常量
LOAD_FAST: 在局部作用域中(比如函數)加載一個當前作用域的局部變量
LOAD_GLOBAL: 在局部作用域(比如函數)中加載一個全局變量或者內置變量
LOAD_NAME: 在全局作用域中加載一個全局變量或者內置變量
STORE_FAST: 在局部作用域中定義一個局部變量, 來建立和某個對象之間的映射關系
STORE_GLOBAL: 在局部作用域中定義一個global關鍵字聲明的全局變量, 來建立和某個對象之間的映射關系
STORE_NAME: 在全局作用域中定義一個全局變量, 來建立和某個對象之間的映射關系
然后下面的我們就來看看這些指令:
name = "夏色祭"
def foo():
gender = "female"
print(gender)
print(name)
import dis
dis.dis(foo)
9 0 LOAD_CONST 1 ('female')
2 STORE_FAST 0 (gender)
10 4 LOAD_GLOBAL 0 (print)
6 LOAD_FAST 0 (gender)
8 CALL_FUNCTION 1
10 POP_TOP
11 12 LOAD_GLOBAL 0 (print)
14 LOAD_GLOBAL 1 (name)
16 CALL_FUNCTION 1
18 POP_TOP
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
0 LOAD_CONST 1 ('female'): 加載常量"female", 所以是LOAD_CONST
2 STORE_FAST 0 (gender): 在局部作用域中定義一個局部變量gender, 所以是STORE_FAST
4 LOAD_GLOBAL 0 (print): 在局部作用域中加載一個內置變量print, 所以是LOAD_GLOBAL
6 LOAD_FAST 0 (gender): 在局部作用域中加載一個局部變量gender, 所以是LOAD_FAST
14 LOAD_GLOBAL 1 (name): 在局部作用域中加載一個全局變量name, 所以是LOAD_GLOBAL
name = "夏色祭"
def foo():
global name
name = "馬自立三舅"
import dis
dis.dis(foo)
10 0 LOAD_CONST 1 ('馬自立三舅')
2 STORE_GLOBAL 0 (name)
4 LOAD_CONST 0 (None)
6 RETURN_VALUE
0 LOAD_CONST 1 ('馬自立三舅'): 加載一個字符串常量, 所以是LOAD_CONST
2 STORE_GLOBAL 0 (name): 在局部作用域中定義一個被global關鍵字聲明的全局變量, 所以是STORE_GLOBAL
s = """
name = "夏色祭"
print(name)
"""
import dis
dis.dis(compile(s, "xxx", "exec"))
2 0 LOAD_CONST 0 ('夏色祭')
2 STORE_NAME 0 (name)
3 4 LOAD_NAME 1 (print)
6 LOAD_NAME 0 (name)
8 CALL_FUNCTION 1
10 POP_TOP
12 LOAD_CONST 1 (None)
14 RETURN_VALUE
0 LOAD_CONST 0 ('夏色祭'): 加載一個字符串常量, 所以是LOAD_CONST
2 STORE_NAME 0 (name): 在全局作用域中定義一個全局變量name, 所以是STORE_NAME
4 LOAD_NAME 1 (print): 在全局作用域中加載一個內置變量print, 所以是LOAD_NAME
6 LOAD_NAME 0 (name): 在全局作用域中加載一個全局變量name, 所以是LOAD_NAME
因此LOAD_CONST、LOAD_FAST、LOAD_GLOBAL、LOAD_NAME、STORE_FAST、STORE_GLOBAL、STORE_NAME它們是和加載常量、變量和定義變量之間有關的,可以說常見的不能再常見了,你寫的任何代碼在反編譯之后都少不了它們的身影,至少會出現一個。因此有必要提前解釋一下,它們分別代表的含義是什么。
Python 中變量交換有兩種不同的寫法,示例如下。這兩種寫法有什么區別嗎?那種寫法更好?
# 寫法一
a, b = b, a
# 寫法二
tmp = a
a = b
b = tmp
這兩種寫法都能實現變量交換,表面上看第一種寫法更加簡潔明了,似乎更優。那么,在優雅的外表下是否隱藏着不為人知的性能缺陷呢?想要找到答案,唯一的途徑是研究字節碼:
# 寫法一
1 0 LOAD_NAME 0 (b)
2 LOAD_NAME 1 (a)
4 ROT_TWO
6 STORE_NAME 1 (a)
8 STORE_NAME 0 (b)
# 寫法二
1 0 LOAD_NAME 0 (a)
2 STORE_NAME 1 (tmp)
2 4 LOAD_NAME 2 (b)
6 STORE_NAME 0 (a)
3 8 LOAD_NAME 1 (tmp)
10 STORE_NAME 2 (b)
從字節碼上看,第一種寫法需要的指令條目要少一些:先將兩個變量依次加載到棧,然后一條 ROT_TWO 指令將棧中的兩個變量交換,最后再將變量依次寫回去。注意到,變量加載的順序與 = 右邊一致,寫回順序與 = 左邊一致。
case TARGET(ROT_TWO): {
//從棧頂彈出元素, 因為棧是先入后出的
//由於b先入棧、a后入棧, 所以這里獲取的棧頂元素就是a
PyObject *top = TOP();
//運行時棧的第二個元素就是b
PyObject *second = SECOND();
//當然棧里面的元素是誰在這里並不重要, 重點是我們看到棧頂元素被設置成了棧的第二個元素
//棧的第二個元素被設置成了棧頂元素, 所以兩個元素確實實現了交換
SET_TOP(second);
SET_SECOND(top);
FAST_DISPATCH();
}
而且,ROT_TWO 指令只是將棧頂兩個元素交換位置,執行起來比 LOAD_NAME 和 STORE_NAME 都要快。
至此,我們可以得到結論了——第一種變量交換寫法更優:
代碼簡潔明了, 不拖泥帶水
不需要輔助變量 tmp, 節約內存
ROT_TWO 指令比 LOAD_NAME 和 STORE_NAME 組成的指令對更有優勢,執行效率更高
請解釋 is 和 == 這兩個操作的區別。
a is b
a == b
我們知道 is 是 對象標識符 ( object identity ),判斷兩個引用是不是引用的同一個對象,等價於 id(a) == id(b) ;而 == 操作符判斷兩個引用所引用的對象是不是相等,等價於調用魔法方法 a.__eq__(b) 。因此,== 操作符可以通過 __eq__ 魔法方法進行覆寫( overriding ),而 is 操作符無法覆寫。
從字節碼上看,這兩個語句也很接近,區別僅在比較指令 COMPARE_OP 的操作數上:
# a is b
1 0 LOAD_NAME 0 (a)
2 LOAD_NAME 1 (b)
4 COMPARE_OP 8 (is)
6 POP_TOP
# a == b
1 0 LOAD_NAME 0 (a)
2 LOAD_NAME 1 (b)
4 COMPARE_OP 2 (==)
6 POP_TOP
COMPARE_OP 指令處理邏輯在 Python/ceval.c 源文件中實現,關鍵函數是 cmp_outcome:
static PyObject *
cmp_outcome(int op, PyObject *v, PyObject *w)
{
//我們說Python中的變量在C的層面上是一個指針, 因此Python中兩個變量是否指向同一個對象 等價於 在C中兩個指針是否相等
//而Python中的==, 則需要調用PyObject_RichCompare(指針1, 指針2, 操作符)來看它們指向的對象所維護的值是否相等
int res = 0;
switch (op) {
case PyCmp_IS:
//is操作符的話, 在C的層面直接一個==判斷即可
res = (v == w);
break;
// ...
default:
//而PyObject_RichCompare是一個函數調用, 將進一步調用對象的魔法方法進行判斷。
return PyObject_RichCompare(v, w, op);
}
v = res ? Py_True : Py_False;
Py_INCREF(v);
return v;
}
舉個栗子:
>>> a = 1024
>>> b = a
>>> a is b
True
>>> a == b
True
>>> # a 和 b 均引用同一個對象, is 和 == 操作均返回 True
>>>
>>>
>>> a = 1024
>>> b = int('1024')
>>> a is b
False
>>> a == b
True
>>> # 顯然, 由於背后對象是不同的, is 操作結果是 False; 而對象值相同, == 操作結果是 True
用一張圖看一下它們之間的區別:
一般而言如果a is b成立,那么a == b多半成立,可能有人好奇,a is b成立說明a和b指向的是同一個對象了,那么a == b表示該對象和自己進行比較,結果為啥不相等呢?以下面兩種情況為例:
重寫了__eq__的類的實例對象
class Girl:
def __eq__(self, other):
return False
g = Girl()
print(g is g) # True
print(g == g) # False
浮點數nan
import math
import numpy as np
a = float("nan")
b = math.nan
c = np.nan
print(a is a, a == a) # True False
print(b is b, b == b) # True False
print(c is c, c == c) # True False
# nan 是一個特殊的 浮點數, 意思是not a number, 表示不是一個數字, 用於表示 異常值, 即不存在或者非法的值
# 不管 nan 跟任何浮點(包括自身)做何種數學比較, 結果均為 False
在 Python 中與 None 比較時,為什么要用 is None 而不是 == None ?
None 是一種特殊的內建對象,它是單例對象,整個運行的程序中只有一個。因此,如果一個變量等於 None,那么is None一定成立,內存地址是相同的。
Python 中的 == 操作符對兩個對象進行相等性比較,背后調用 __eq__ 魔法方法。在自定義類中,__eq__ 方法可以被覆寫:
class Girl:
def __eq__(self, other):
return True
g = Girl()
print(g is None) # False
print(g == None) # True
而且最重要的一點,我們在介紹is和 == 之間區別的時候說過,Python的is在底層是比較地址是否相等,所以對於C而言只是判斷兩個變量間是否相等、一個 == 操作符即可;但是對於Python的==,在底層則是需要調用PyObject_RichCompare函數,然后進一步取出所維護的值進行比較。所以通過is None來判斷會在性能上更有優勢一些,再加上None是單例對象,使用is判斷是最合適的。我們使用jupyter notebook測試一下兩者的性能吧:
%timeit name is None
31.6 ns ± 1.62 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit name == None
36.6 ns ± 2.8 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
復雜內建對象的創建
像整數對象、字符串對象在創建時的字節碼,相信都已經理解了。總共兩條指令:直接先LOAD常量,然后STORE(兩者組成entry放在local名字空間中)
。
但是問題來了,像列表、字典這樣的對象,底層是怎么創建的呢?顯然它們的創建要更復雜一些,兩條指令是不夠的。下面我們就來看看列表、字典在創建時對應的字節碼是怎樣的吧。
不過在此之前我們需要看一些宏,這是PyFrame_EvalFrameEx(調用了_PyEval_EvalFrameDefault)
在遍歷指令序列co_code時所需要的宏,里面包括了對棧的各種操作,以及對PyTupleObject對象的元素的訪問操作。
//獲取PyTupleObject對象中指定索引對應的元素
#ifndef Py_DEBUG
#define GETITEM(v, i) PyTuple_GET_ITEM((PyTupleObject *)(v), (i))
#else
#define GETITEM(v, i) PyTuple_GetItem((v), (i))
#endif
//調整棧頂指針, 這個stack_pointer指向運行時棧的頂端
#define BASIC_STACKADJ(n) (stack_pointer += n)
#define STACKADJ(n) { (void)(BASIC_STACKADJ(n), \
lltrace && prtrace(TOP(), "stackadj")); \
assert(STACK_LEVEL() <= co->co_stacksize); }
//入棧操作
#define BASIC_PUSH(v) (*stack_pointer++ = (v))
#define PUSH(v) BASIC_PUSH(v)
//出棧操作
#define BASIC_POP() (*--stack_pointer)
#define POP() ((void)(lltrace && prtrace(TOP(), "pop")), \
BASIC_POP())
然后我們隨便創建一個列表和字典吧。
s = """
lst = [1, 2, "3", "xxx"]
d = {"name": "夏色祭", "age": -1}
"""
import dis
dis.dis(compile(s, "xxx", "exec"))
2 0 LOAD_CONST 0 (1)
2 LOAD_CONST 1 (2)
4 LOAD_CONST 2 ('3')
6 LOAD_CONST 3 ('xxx')
8 BUILD_LIST 4
10 STORE_NAME 0 (lst)
3 12 LOAD_CONST 4 ('夏色祭')
14 LOAD_CONST 5 (-1)
16 LOAD_CONST 6 (('name', 'age'))
18 BUILD_CONST_KEY_MAP 2
20 STORE_NAME 1 (d)
22 LOAD_CONST 7 (None)
24 RETURN_VALUE
首先對於列表來說,它是先將列表中的常量加載進來了,從上面的4個LOAD_CONST也能看出來。然后重點來了,我們看到有一行指令 BUILD_LIST 4,從名字上也能看出來這是要根據load進行來的4個常量創建一個列表,后面的4表示這個列表有4個元素。
但是問題來了,Python怎么知道這構建的是一個列表呢?元組難道不可以嗎?答案是因為我們創建的是列表,不是元組,而且這個信息也體現在了字節碼中。然后我們看看BUILD_LIST都干了些什么吧。
case TARGET(BUILD_LIST): {
//這里的oparg顯然指的就是BUILD_LIST后面的4
//因此可以看到這個oparg的含義取決於字節碼指令, 比如:LOAD_CONST就是代表索引, 這里的就是列表元素個數
//PyList_New表示創建一個能容納4個元素的的PyListObject對象
PyObject *list = PyList_New(oparg);
if (list == NULL)
goto error;
//從運行時棧里面將元素一個一個的彈出來, 注意它的索引, load元素的時候是按照1、2、"3"、"xxx"的順序load
while (--oparg >= 0) {
//但是棧是先入后出結構, 索引棧頂的元素是"xxx", 棧底的元素是1
//所以這里彈出元素的順序就變成了"xxx"、"3"、2、1
PyObject *item = POP();
//所以這里的oparg是從后往前遍歷的, 即3、2、1、0
//所以最終將"xxx"設置在索引為3的位置、將"3"設置在索引2位的位置, 將2設置在索引為1的位置, 將1設置在索引為0的位置
//這顯然是符合我們的預期的
PyList_SET_ITEM(list, oparg, item);
}
//構建完畢之后, 將其壓入運行時棧, 此時棧中只有一個PyListObject對象, 因為先load進來的4個常量在構建列表的時候已經被逐個彈出來了
PUSH(list);
DISPATCH();
}
但BUILD_LIST之后,只改變了運行時棧,沒有改變local空間。所以后面的STORE_NAME 0 (lst)表示將在local空間中建立一個 "符號lst" 到 "BUILD_LIST構建的PyListObject對象" 之間的映射,也就是組合成一個entry放在local空間中,這樣我們后面才可以通過符號lst找到對應的列表。
STORE_NAME我們已經見過了,這里就不說了。其實STORE_XXX和LOAD_XXX都是非常簡單的,像LOAD_GLOBAL、LOAD_FAST、STORE_FAST等等可以自己去看一下,沒有任何難度,當然我們下面也會說。
2 0 LOAD_CONST 0 (1)
2 LOAD_CONST 1 (2)
4 LOAD_CONST 2 ('3')
6 LOAD_CONST 3 ('xxx')
8 BUILD_LIST 4
10 STORE_NAME 0 (lst)
3 12 LOAD_CONST 4 ('夏色祭')
14 LOAD_CONST 5 (-1)
16 LOAD_CONST 6 (('name', 'age'))
18 BUILD_CONST_KEY_MAP 2
20 STORE_NAME 1 (d)
22 LOAD_CONST 7 (None)
24 RETURN_VALUE
然后我們再看看字典的構建方式,首先依舊是加載兩個常量,顯然這個字典是value。然后注意:我們看到key是作為一個元組加載進來的。而且如果我們創建了一個元組,那么這個元組也會整體被LOAD_CONST,所以從這里我們也能看到列表和元組之間的區別,列表的元素是一個一個加載的,元組是整體加載的,只需要LOAD_CONST一次即可。BUILD_CONST_KEY_MAP 2毋庸置疑就是構建一個字典了,后面的oparg是2,表示這個字典有兩個entry,我們看一下源碼:
case TARGET(BUILD_CONST_KEY_MAP): {
Py_ssize_t i; //循環變量
PyObject *map;//一個PyDictObject對象指針
PyObject *keys = TOP();//從棧頂獲取所有的key, 一個元組
//如果keys不是一個元組或者這個元組的ob_size不等於oparg, 那么表示字典構建失敗
if (!PyTuple_CheckExact(keys) ||
PyTuple_GET_SIZE(keys) != (Py_ssize_t)oparg) {
_PyErr_SetString(tstate, PyExc_SystemError,
"bad BUILD_CONST_KEY_MAP keys argument");
//顯然這是屬於Python內部做的處理, 至少我們在使用層面沒有遇到過這個問題
goto error;
}
//申請一個字典, 表示至少要容納oparg個鍵值對, 但是具體的容量肯定是要大於oparg的
//至於到底是多少, 則取決於oparg, 總之這一步就是申請合適容量的字典
map = _PyDict_NewPresized((Py_ssize_t)oparg);
if (map == NULL) {
goto error;
}
//很明顯, 這里開始循環了, 要依次設置鍵值對了
//還記得在BUILD_CONST_KEY_MAP之前, 常量是怎么加載的嗎?是按照"夏色祭"、-1、('name', 'age')的順序加載的
//所以棧里面的元素, 從棧頂到棧底就應該是('name', 'age')、-1、"夏色祭"
for (i = oparg; i > 0; i--) {
int err;
//這里是獲取元組里面的元素, 也就是key, 注意: 索引是oparg - i, 而i是從oparg開始自減的
//以當前為例, 循環結束時, oparg - i分別是0、1,那么獲取的元素顯然就分別是: "name"、"age"
PyObject *key = PyTuple_GET_ITEM(keys, oparg - i);
//然后這里的PEEK和TOP類似, 都是獲取元素但是不從棧里面刪除, TOP是專門獲取棧頂元素, PEEK還可以獲取棧的其它位置的元素
//而這里獲取也是按照"夏色祭"、-1的順序獲取, 和"name"、"age"之間是正好對應的
PyObject *value = PEEK(i + 1);
//然后將entry設置在map里面
err = PyDict_SetItem(map, key, value);
if (err != 0) {
Py_DECREF(map);
goto error;
}
}
//依次清空運行時棧, 將棧里面的元素挨個彈出來
Py_DECREF(POP());
while (oparg--) {
Py_DECREF(POP());
}
//將構建的PyDictObject對象壓入運行時棧
PUSH(map);
DISPATCH();
}
最后STORE_NAME 1 (d),顯然是再將運行時棧中的字典彈出來,將符號d和彈出來的字典建立一個entry放在local空間中。
在所有的字節碼指令都執行完畢之后,運行時棧會是空的,但是所有的信息都存儲在了local名字空間中。
函數中的變量
我們之前定義的變量是在模塊級別的作用域中,但如果我們在函數中定義呢?
def foo():
i = 1
s = "python"
2 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (i)
3 4 LOAD_CONST 2 ('python')
6 STORE_FAST 1 (s)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
我們看到大致一樣,但是有一點發生了變化, 那就是在將變量名和變量值映射的時候,使用的不再是STORE_NAME,而是STORE_FAST,顯然STORE_FAST會更快一些。為什么這么說,這是因為函數中的局部變量總是固定不變的,在編譯的時候就能確定局部變量使用的內存空間的位置,也能確定局部變量字節碼指令應該如何訪問內存,就能使用靜態的方法來實現局部變量。其實局部變量的讀寫都在fastlocals = f -> f_localsplus
上面。
case TARGET(STORE_FAST) {
PyObject *value = POP();
SETLOCAL(oparg, value);
FAST_DISPATCH();
}
#define SETLOCAL(i, value) do { PyObject *tmp = GETLOCAL(i); \
GETLOCAL(i) = value; \
Py_XDECREF(tmp); } while (0)
#define GETLOCAL(i) (fastlocals[i])
一般表達式
符號搜索
我們還是舉個例子:
a = 5
b = a
1 0 LOAD_CONST 0 (5)
2 STORE_NAME 0 (a)
2 4 LOAD_NAME 0 (a)
6 STORE_NAME 1 (b)
8 LOAD_CONST 1 (None)
10 RETURN_VALUE
首先源代碼第一行對應的字節碼指令無需介紹,但是第二行對應的指令變了,我們看到不再是LOAD_CONST,而是LOAD_NAME。其實也很好理解,第一行a = 5,而5是一個常量所以是LOAD_CONST,但是b = a,這里的a是一個變量名,所以是LOAD_NAME。
//這里顯然要從幾個名字空間里面去尋找指定的變量名對應的值
//找不到就會出現NameError
case TARGET(LOAD_NAME) {
//從符號表里面獲取變量名
PyObject *name = GETITEM(names, oparg);
//獲取local命名空間, 一個PyDictObject對象
PyObject *locals = f->f_locals;
PyObject *v; //value
if (locals == NULL) {
PyErr_Format(PyExc_SystemError,
"no locals when loading %R", name);
goto error;
}
//根據變量名從locals里面獲取對應的value
if (PyDict_CheckExact(locals)) {
v = PyDict_GetItem(locals, name);
Py_XINCREF(v);
}
else {
v = PyObject_GetItem(locals, name);
if (v == NULL) {
if (!PyErr_ExceptionMatches(PyExc_KeyError))
goto error;
PyErr_Clear();
}
}
//如果v是NULL,說明local名字空間里面沒有
if (v == NULL) {
//於是從global名字空間里面找
v = PyDict_GetItem(f->f_globals, name);
Py_XINCREF(v);
//如果v是NULL說明global里面也沒有
if (v == NULL) {
//下面的if和else里面的邏輯基本一致,只不過對builtin做了檢測
if (PyDict_CheckExact(f->f_builtins)) {
//local、global都沒有,於是從builtin里面找
v = PyDict_GetItem(f->f_builtins, name);
//還沒有,NameError
if (v == NULL) {
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG, name);
goto error;
}
Py_INCREF(v);
}
else {
//從builtin里面找
v = PyObject_GetItem(f->f_builtins, name);
if (v == NULL) {
//還沒有,NameError
if (PyErr_ExceptionMatches(PyExc_KeyError))
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG, name);
goto error;
}
}
}
}
//找到了,把v給push進去,相當於壓棧
PUSH(v);
DISPATCH();
}
另外如果是在函數里面,那么b = a就既不是LOAD_CONST、也不是LOAD_NAME,而是LOAD_FAST。這是因為函數中的變量在編譯的時候就已經確定,因此是LOAD_FAST。那么如果b = a在函數里面,而a = 5定義在函數外面呢?那么結果是LOAD_GLOBAL,因為知道這個a到底是定義在什么地方。
數值運算
a = 5
b = a
c = a + b
1 0 LOAD_CONST 0 (5)
2 STORE_NAME 0 (a)
2 4 LOAD_NAME 0 (a)
6 STORE_NAME 1 (b)
3 8 LOAD_NAME 0 (a)
10 LOAD_NAME 1 (b)
12 BINARY_ADD
14 STORE_NAME 2 (c)
16 LOAD_CONST 1 (None)
18 RETURN_VALUE
顯然這里我們直接從 8 LOAD_NAME開始看即可,首先是加在兩個變量,然后通過BINARY_ADD進行加法運算。
case TARGET(BINARY_ADD) {
//獲取兩個值,也就是我們a和b對應的值, a是棧底、b是棧頂
PyObject *right = POP(); //從棧頂彈出b
PyObject *left = TOP(); //彈出b之后, 此時a就成為了棧頂, 直接通過TOP獲取, 但是不彈出
PyObject *sum;
//這里檢測是否是字符串
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
//是的話直接拼接
sum = unicode_concatenate(left, right, f, next_instr);
}
else {
//不是的話相加
sum = PyNumber_Add(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
//設置sum, 將棧頂的元素(之前的a)給頂掉
SET_TOP(sum);
if (sum == NULL)
goto error;
DISPATCH();
}
信息輸出
最后看看信息是如何輸出的:
a = 5
b = a
c = a + b
print(c)
1 0 LOAD_CONST 0 (5)
2 STORE_NAME 0 (a)
2 4 LOAD_NAME 0 (a)
6 STORE_NAME 1 (b)
3 8 LOAD_NAME 0 (a)
10 LOAD_NAME 1 (b)
12 BINARY_ADD
14 STORE_NAME 2 (c)
4 16 LOAD_NAME 3 (print)
18 LOAD_NAME 2 (c)
20 CALL_FUNCTION 1
22 POP_TOP
24 LOAD_CONST 1 (None)
26 RETURN_VALUE
我們直接從16 LOAD_NAME開始看,首先從builtins中加載變量print(本質上加載和變量綁定的對象)
,然后加載變量c,將兩者壓入運行時棧。
CALL_FUNCTION,表示函數調用,執行剛才的print,后面的1則是參數的個數。另外,當調用print的時候,實際上又創建了一個棧幀,因為只要是函數調用都會創建棧幀的。
case TARGET(CALL_FUNCTION) {
PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
goto error;
}
DISPATCH();
}
然后POP_TOP表示從棧的頂端彈出函數的返回值,因為POP_TOP的上一步是一個call_function,也就是函數調用。而函數是有返回值的,在函數調用(call_function指令)執行完畢之后會自動將返回值設置在棧頂,而POP_TOP就是負責將上一步函數調用的返回值從棧頂彈出來。只不過我們這里是print函數返回的是None、我們不需要這個返回值,或者說我們沒有使用變量接收,所以直接將其從棧頂彈出去即可。但如果我們是res = print(c),那么你會發現指令POP_TOP就變成了STORE_NAME,因為要將符號和返回值綁定起來放在local空間中。最后LOAD_CONST、RETURN_VALUE,無需解釋了,就是返回值,不光是函數,類代碼塊、模塊代碼塊在執行完畢之后也會返回一個值給調用者,只不過這個值通常是None。最后再來看看print是如何打印的:
//Objects/bltinmodule.c
static PyObject *
builtin_print(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
//Python里面print支持的參數, 這里是解析我們在調用print所傳遞的位置參數和關鍵字參數
static const char * const _keywords[] = {"sep", "end", "file", "flush", 0};
static struct _PyArg_Parser _parser = {"|OOOO:print", _keywords, 0};
//初始化全部為NULL
PyObject *sep = NULL, *end = NULL, *file = NULL, *flush = NULL;
int i, err;
if (kwnames != NULL &&
!_PyArg_ParseStackAndKeywords(args + nargs, 0, kwnames, &_parser,
&sep, &end, &file, &flush)) {
return NULL;
}
//file參數
if (file == NULL || file == Py_None) {
file = _PySys_GetObjectId(&PyId_stdout);
//默認輸出到sys.stdout也就是控制台
if (file == NULL) {
PyErr_SetString(PyExc_RuntimeError, "lost sys.stdout");
return NULL;
}
/* sys.stdout may be None when FILE* stdout isn't connected */
if (file == Py_None)
Py_RETURN_NONE;
}
//sep分隔符, 默認是空格
if (sep == Py_None) {
sep = NULL;
}
else if (sep && !PyUnicode_Check(sep)) {
PyErr_Format(PyExc_TypeError,
"sep must be None or a string, not %.200s",
sep->ob_type->tp_name);
return NULL;
}
//end, 默認是換行
if (end == Py_None) {
end = NULL;
}
else if (end && !PyUnicode_Check(end)) {
PyErr_Format(PyExc_TypeError,
"end must be None or a string, not %.200s",
end->ob_type->tp_name);
return NULL;
}
//將里面的元素逐個打印到file中
for (i = 0; i < nargs; i++) {
if (i > 0) {
if (sep == NULL)
//設置sep為空格
err = PyFile_WriteString(" ", file);
else
//否則說明用戶了sep
err = PyFile_WriteObject(sep, file,
Py_PRINT_RAW);
if (err)
return NULL;
}
err = PyFile_WriteObject(args[i], file, Py_PRINT_RAW);
if (err)
return NULL;
}
//end同理,不指定的話默認是打印換行
if (end == NULL)
err = PyFile_WriteString("\n", file);
else
err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
if (err)
return NULL;
//flush表示是否強制刷新控制台
if (flush != NULL) {
PyObject *tmp;
int do_flush = PyObject_IsTrue(flush);
if (do_flush == -1)
return NULL;
else if (do_flush) {
tmp = _PyObject_CallMethodId(file, &PyId_flush, NULL);
if (tmp == NULL)
return NULL;
else
Py_DECREF(tmp);
}
}
Py_RETURN_NONE;
}
小結
這次我們就簡單分析了一下字節碼指令,介紹了一些常見的指令。