楔子
Python 現在如此流行,擁有眾多開源、高質量的第三方庫是一個重要原因,不過 Python 的簡單、靈巧、容易上手也是功不可沒的,而其背后的內置函數(類)則起到了很大的作用。舉個栗子:
numbers = [1, 2, 3, 4, 5]
# 將里面每一個元素都加1
print(list(map(lambda x: x + 1, numbers))) # [2, 3, 4, 5, 6]
strings = ["abc", "d", "def", "kf", "ghtc"]
# 篩選出長度大於等於3的
print(list(filter(lambda x: len(x) >= 3, strings))) # ['abc', 'def', 'ghtc']
keys = ["name", "age", "gender"]
values = ["夏色祭", 17, "female"]
# 將keys 和 values 里面的元素按照順序組成字典
print(dict(zip(keys, values))) # {'name': '夏色祭', 'age': 17, 'gender': 'female'}
我們看到一行代碼就搞定了,那么問題來了,這些內置函數(類)在底層是怎么實現的呢?下面我們就來通過源碼來分析一下,這里我們介紹 map、filter、zip。
首先這些類(map、filter、zip都是類)都位於 builtin 名字空間中,而我們之前在介紹源碼的時候提到過一個文件:Python/bltinmodule.c,我們說該文件是和內置函數(類)相關的,那么顯然 map、filter、zip 也藏身於此。
Python已經進化到 3.9.0 了,所以這里就使用 Python3.9.0 的源碼進行分析吧。
map底層實現
我們知道map是將一個序列中的每個元素都作用於同一個函數(當然類、方法也可以):
當然,我們知道調用map的時候並沒有馬上執行上面的操作,而是返回一個map對象。既然是對象,那么底層必有相關的定義。
typedef struct {
PyObject_HEAD
PyObject *iters;
PyObject *func;
} mapobject;
PyObject_HEAD:見過很多次了,它是Python中任何對象都會有的頭部信息。包含一個引用計數ob_refcnt、和一個類型對象的指針ob_type;
iters:一個指針,這里實際上是一個PyTupleObject *,以
map(lambda x: x + 1, [1, 2, 3])
為例,那么這里的 iters 就相當於是([1, 2, 3, 4, 5].__iter__(),)
。至於為什么,分析源碼的時候就知道了;func:顯然就是函數指針了,PyFunctionObject *;
通過底層結構體定義,我們也可以得知在調用map時並沒有真正的執行;對於函數和可迭代對象,只是維護了兩個指針去指向它。
而一個PyObject占用16字節,再加上兩個8字節的指針總共32字節。因此在64位機器上,任何一個map對象所占大小都是32字節。
numbers = list(range(100000))
strings = ["abc", "def"]
# 都占32字節
print(map(lambda x: x * 3, numbers).__sizeof__()) # 32
print(map(lambda x: x * 3, strings).__sizeof__()) # 32
再來看看map的用法,Python中的 map 不僅可以作用於一個序列,還可以作用於任意多個序列。
m1 = map(lambda x: x[0] + x[1] + x[2], [(1, 2, 3), (2, 3, 4), (3, 4, 5)])
print(list(m1)) # [6, 9, 12]
# map 還可以接收任意個可迭代對象
m2 = map(lambda x, y, z: x + y + z, [1, 2, 3], [2, 3, 4], [3, 4, 5])
print(list(m2)) # [6, 9, 12]
# 所以底層結構體中的iters在這里就相當於 ([1, 2, 3].__iter__(), [2, 3, 4].__iter__(), [3, 4, 5].__iter__())
# 我們說map的第一個參數是一個函數, 后面可以接收任意多個可迭代對象
# 但是注意: 可迭代對象的數量 和 函數的參數個數 一定要匹配
m3 = map(lambda x, y, z: str(x) + y + z, [1, 2, 3], ["a", "b", "c"], "abc")
print(list(m3)) # ['1aa', '2bb', '3cc']
# 但是可迭代對象之間的元素個數不要求相等, 會以最短的為准
m4 = map(lambda x, y, z: x + y + z, [1, 2, 3], [2, 3], [3, 4, 5])
print(list(m4)) # [6, 9]
# 當然也支持更加復雜的形式
m5 = map(lambda x, y: x[0] + x[1] + y, [(1, 2), (2, 3)], [3, 4])
print(list(m5)) # [6, 9]
所以我們看到 map 會將后面所有可迭代對象中的每一個元素按照順序依次取出,然后傳遞到函數中,因此函數的參數個數 和 可迭代對象的個數 一定要相等。
那么map對象在底層是如何創建的呢?很簡單,因為map是一個類,那么調用的時候一定會執行里面的 __new__ 方法。
static PyObject *
map_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyObject *it, *iters, *func;
mapobject *lz;
Py_ssize_t numargs, i;
// map對象在底層對應的是 mapobject、map類本身在底層對應的則是 PyMap_Type
// _PyArg_NoKeywords表示檢驗是否沒有傳遞關鍵字參數, 如果沒傳遞, 那么結果為真; 傳遞了, 結果為假;
if (type == &PyMap_Type && !_PyArg_NoKeywords("map", kwds))
// 可以看到 map 不接受關鍵字參數
// 如果傳遞了, 那么會報如下錯誤: TypeError: map() takes no keyword arguments, 可以自己嘗試一下
return NULL;
// 位置參數都在 args 里面, 上面的 kwds 是關鍵字參數
// 這里獲取位置參數的個數, 1個函數、numargs - 1個可迭代對象, 這里的args 是一個 PyTupleObject *
numargs = PyTuple_Size(args);
// 如果參數個數小於2
if (numargs < 2) {
// 拋出 TypeError, 表示 map 至少接受兩個位置參數: 一個函數 和 至少一個可迭代對象
PyErr_SetString(PyExc_TypeError,
"map() must have at least two arguments.");
return NULL;
}
// 申請一個元組, 容量為 numargs - 1, 用於存放傳遞的所有可迭代對象(對應的迭代器)
iters = PyTuple_New(numargs-1);
// 為NULL表示申請失敗
if (iters == NULL)
return NULL;
// 依次循環
for (i=1 ; i<numargs ; i++) {
// PyTuple_GET_ITEM(args, i) 表示獲取索引為 i 的可迭代對象
// PyObject_GetIter 表示獲取對應的迭代器, 相當於內置函數 iter
it = PyObject_GetIter(PyTuple_GET_ITEM(args, i));
// 為NULL表示獲取失敗, 但是iters這個元組已經申請了, 所以減少其引用計數, 將其銷毀
if (it == NULL) {
Py_DECREF(iters);
return NULL;
}
// 將對應的迭代器設置在元組iters中
PyTuple_SET_ITEM(iters, i-1, it);
}
// 調用 PyMap_Type 的 tp_alloc, 為其實例對象申請空間
lz = (mapobject *)type->tp_alloc(type, 0);
// 為NULL表示申請失敗, 減少iters的引用計數
if (lz == NULL) {
Py_DECREF(iters);
return NULL;
}
// 讓lz的iters成員 等於 iters
lz->iters = iters;
// 獲取第一個參數, 也就是函數
func = PyTuple_GET_ITEM(args, 0);
// 增加引用計數, 因為該函數被作為參數傳遞給map了
Py_INCREF(func);
// 讓lz的func成員 等於 func
lz->func = func;
// 轉成 PyObject *泛型指針, 然后返回
return (PyObject *)lz;
}
所以我們看到map_new做的工作很簡單,就是實例化一個map對象,然后對內部的成員進行賦值。我們用Python來模擬一下上述過程:
class MyMap:
def __new__(cls, *args, **kwargs):
if kwargs:
raise TypeError("MyMap不接受關鍵字參數")
numargs = len(args)
if numargs < 2:
raise TypeError("MyMap至少接收兩個參數")
# 元組內部的元素不可以改變(除非本地修改), 所以這里使用列表來模擬
iters = [None] * (numargs - 1) # 創建一個長度為 numargs - 1 的列表, 元素都是None, 模擬C中的NULL
i = 1
while i < numargs: # 逐步循環
it = iter(args[i]) # 獲取可迭代對象, 得到其迭代器
iters[i - 1] = it # 設置在 iters 中
i += 1
# 為實例對象申請空間
instance = object.__new__(cls)
# 設置成員
instance.iters = iters
instance.func = args[0]
return instance # 返回實例對象
m = MyMap(lambda x, y: x + y, [1, 2, 3], [11, 22, 33])
print(m) # <__main__.MyMap object at 0x00000167F4552E80>
print(m.func) # <function <lambda> at 0x0000023ABC4C51F0>
print(m.func(2, 3)) # 5
print(m.iters) # [<list_iterator object at 0x0000025F13AF2940>, <list_iterator object at 0x0000025F13AF2CD0>]
print([list(it) for it in m.iters]) # [[1, 2, 3], [11, 22, 33]]
我們看到非常簡單,這里我們沒有設置構造函數__init__,這是因為 map 內部沒有 __init__,它的成員都是在__new__里面設置的。
# map的__init__ 實際上就是 object的__init__
print(map.__init__ is object.__init__) # True
print(map.__init__) # <slot wrapper '__init__' of 'object' objects>
事實上,你會發現map對象非常類似迭代器,而事實上它們也正是迭代器。
from typing import Iterable, Iterator
m = map(str, [1, 2, 3])
print(isinstance(m, Iterable)) # True
print(isinstance(m, Iterator)) # True
為了能更方便地理解后續內容,這里我們來提一下Python中的迭代器,可能有人覺得Python的迭代器很神奇,但如果你看了底層實現的話,你肯定會覺得:"就這?"
// Objects/iterobject.c
typedef struct {
PyObject_HEAD
Py_ssize_t it_index; // 每迭代一次, index自增1
PyObject *it_seq; // 走到頭之后, 將it_seq設置為NULL
} seqiterobject;
這就是Python的迭代器,非常簡單,我們直接用Python來模擬一下:
class MyIterator:
def __new__(cls, it_seq):
instance = object.__new__(cls) # 創建實例對象
instance.it_index = 0
instance.it_seq = it_seq
return instance
def __iter__(self):
return self
def __next__(self):
# 如果 self.it_seq 為None, 證明此迭代器已經迭代完畢
if self.it_seq is None:
raise StopIteration
try:
# 逐步迭代, 說白了就是使用索引獲取, 每迭代一次、索引自增1
val = self.it_seq[self.it_index]
self.it_index += 1
return val
except IndexError:
# 出現索引越界, 證明已經遍歷完畢
# 直接將 self.it_seq 設置為None即可
raise StopIteration
for _ in MyIterator([1, 2, 3]):
print(_, end=" ") # 1 2 3
print()
my_it = MyIterator([2, 3, 4])
# 只能迭代一次
print(list(my_it)) # [2, 3, 4]
print(list(my_it)) # []
Python的迭代器底層就是這么做的,可能有人覺得這不就是把 可迭代對象 和 索引 進行了一層封裝嘛。每迭代一次,索引自增1,當出現索引越界時,證明迭代完了,直接將 it_seq 設置為 NULL 即可(這也側面說明了為什么迭代器從開始到結束只能迭代一次)。
是的,迭代器就是這么簡單,沒有一點神秘。當然不僅是迭代器,再比如關鍵字 in,在C的層面其實就是一層 for 循環罷了。而迭代器除了通過 __iter__ 實現之外,我們還可以通過 __getitem__,__iter__ 我們下一章分析,下面看看 __getitem__ 在源碼中是如何體現的:
// Objects/iterobject.c
// 創建
PyObject *
PySeqIter_New(PyObject *seq)
{
// 迭代器
seqiterobject *it;
// 如果不是一個序列的話, 那么調用失敗
if (!PySequence_Check(seq)) {
PyErr_BadInternalCall();
return NULL;
}
// 申請空間
it = PyObject_GC_New(seqiterobject, &PySeqIter_Type);
// 為NULL表示申請失敗
if (it == NULL)
return NULL;
// it_index 初始化為0
it->it_index = 0;
// 因為seq被傳遞了, 所以指向的對象的引用計數要加1
Py_INCREF(seq);
// 將成員it_seq初始化為seq
it->it_seq = seq;
// 將該迭代器對象鏈接到 第0代鏈表 中, 並由GC負責跟蹤(此處和垃圾回收機制相關, 這里不做過多介紹)
_PyObject_GC_TRACK(it);
// 返回迭代器對象
return (PyObject *)it;
}
// 迭代
static PyObject *
iter_iternext(PyObject *iterator)
{
seqiterobject *it; // 迭代器對象
PyObject *seq; // 迭代器對象內部的可迭代對象
PyObject *result; // 迭代結果
assert(PySeqIter_Check(iterator)); // 一定是迭代器
it = (seqiterobject *)iterator; // 將泛型指針PyObject * 轉成 seqiterobject *
seq = it->it_seq; // 獲取內部可迭代對象
// 如果是NULL, 那么證明此迭代器已經迭代完畢, 直接返回NULL
if (seq == NULL)
return NULL;
// 索引達到了最大值, 因為容器內部的元素個數是有限制的; 但如果不是吃撐了寫惡意代碼, 這個限制幾乎不可能會觸發
if (it->it_index == PY_SSIZE_T_MAX) {
PyErr_SetString(PyExc_OverflowError,
"iter index too large");
return NULL;
}
// 根據索引獲取 seq 內部的元素
result = PySequence_GetItem(seq, it->it_index);
// 如果不為NULL, 證明確實迭代出了元素
if (result != NULL) {
// 索引自增1
it->it_index++;
// 然后返回結果
return result;
}
// 當result為NULL的時候, 證明出異常了, 也說明遍歷到頭了
// 進行異常匹配, 如果出現的異常能匹配 IndexError 或者 StopIteration
if (PyErr_ExceptionMatches(PyExc_IndexError) ||
PyErr_ExceptionMatches(PyExc_StopIteration))
{
// 那么不會讓異常拋出, 而是通過 PyErr_Clear() 將異常回溯棧清空
// 所以使用 for i in 迭代器, 或者 list(迭代器) 等等不會報錯, 原因就在於此; 盡管它們也是不斷迭代, 但是在最后會捕獲異常
PyErr_Clear();
// 將it_seq設置為NULL, 表示此迭代器大限已至、油盡燈枯
it->it_seq = NULL;
// 因為將it_seq賦值NULL, 那么原來的可迭代對象就少了一個引用, 因此要將引用計數減1
Py_DECREF(seq);
}
return NULL;
}
所以這就是迭代器,真的一點都不神秘。
在迭代器上面多扯皮了一會兒,但這肯定是值得的,那么回到主題。我們說調用map只是得到一個map對象,從上面的分析我們也可以得出,在整個過程並沒有進行任何的計算。如果要計算的話,我們可以調用__next__、或者使用for循環等等。
m = map(lambda x: x + 1, [1, 2, 3, 4, 5])
print([i for i in m]) # [2, 3, 4, 5, 6]
# 當然我們知道 for 循環的背后本質上會調用迭代器的 __next__
m = map(lambda x: int(x) + 1, "12345")
while True:
try:
print(m.__next__())
except StopIteration:
break
"""
2
3
4
5
6
"""
# 當然上面都不是最好的方式
# 如果只是單純的將元素迭代出來, 而不做任何處理的話, 那么交給tuple、list、set等類型對象才是最佳的方式
# 像tuple(m)、list(m)、set(m)等等
# 所以如果你是[x for x in it]這種做法的話, 那么更建議你使用list(m), 效率會更高, 因為它用的是C中的for循環
# 當然不管是哪種做法, 底層都是一個不斷調用__next__、逐步迭代的過程
所以下面我們來看看map底層是怎么做的?
static PyObject *
map_next(mapobject *lz)
{
// small_stack顯然是一個數組, 里面存放 PyObject *, 顯然它用來存放 map 中所有可迭代對象的索引為i(i=0,1,2,3...)的元素
// 但這個_PY_FASTCALL_SMALL_STACK是什么呢? 我們需要詳細說一下
PyObject *small_stack[_PY_FASTCALL_SMALL_STACK];
/*
_PY_FASTCALL_SMALL_STACK 是一個宏, 定義在 Include/cpython/abstract.h 中, 結果就等於5
small_stack這個數組會首先嘗試在棧區分配,如果通過位置參數來調用一個函數的話, 可以不用申請在堆區
但是數量不能過大, 官方將這個值設置成5, 如果參數個數小於等於5的話, 便可申請在棧中
然后通過傳遞位置參數的方式對函數進行調用, 在C中調用一個 Python函數 有很多種方式;
這里會通過 PyObject_Vectorcall 系列函數(矢量調用, 會更快) 來對函數進行調用, 是的,調用一個函數需要借助另一個函數
之所以將其設置成5, 是為了不濫用C的棧, 從而減少棧溢出的風險
*/
// 二級指針, 指向 small_stack 數組的首元素, 所以是 PyObject **
PyObject **stack;
// 函數調用的返回值
PyObject *result = NULL;
// 獲取當前的線程狀態對象
PyThreadState *tstate = _PyThreadState_GET();
// 獲取iters內置迭代器的數量, 同時也是調用函數時的參數數量
const Py_ssize_t niters = PyTuple_GET_SIZE(lz->iters);
// 如果這個參數小於等於5, 那么在獲取這些迭代器中的元素時, 可以直接使用在C棧中申請的數組進行存儲
if (niters <= (Py_ssize_t)Py_ARRAY_LENGTH(small_stack)) {
stack = small_stack;
}
else {
// 如果超過了5, 那么不好意思, 只能在堆區重新申請了
stack = PyMem_Malloc(niters * sizeof(stack[0]));
// 返回NULL, 表示申請失敗, 說明沒有內存了
if (stack == NULL) {
// 這里傳入線程狀態對象, 會在內部設置異常
_PyErr_NoMemory(tstate);
return NULL;
}
}
// 走到這里說明一切順利, 那么下面就開始迭代了
// 如果是 map(func, [1, 2, 3], ["xx", "yy", "zz"], [2, 3, 4]), 那么第一次迭代出來的元素就是 (1, "xx", 2)
Py_ssize_t nargs = 0;
for (Py_ssize_t i=0; i < niters; i++) {
// 獲取索引為i對應的迭代器,
PyObject *it = PyTuple_GET_ITEM(lz->iters, i);
// Py_TYPE表示獲取對象的 ob_type(類型對象), 然后調用tp_iternext成員進行迭代
// 類似於 type(it).__next__(it)
PyObject *val = Py_TYPE(it)->tp_iternext(it);
// 如果val為NULL, 直接跳轉到 exit 這個label中
if (val == NULL) {
goto exit;
}
// 將 val 設置在數組索引為i的位置中, 然后進行下一輪循環, 也就是獲取下一個迭代器中的元素設置在數組stack中
stack[i] = val;
// nargs++, 和參數個數、迭代器個數 保持一致
// 如果可迭代對象個數是3, 那么小於5, 所以stack會申請在棧區; 但是在棧區申請的話, 長度默認為5, 因此后兩個是元素是無效的
// 所以在調用的時候需要指定有效的參數個數
nargs++;
}
// 進行調用, 得到結果, 這個函數是Python3.9新增的; 如果是Python3.8的話, 調用的是_PyObject_FastCall
result = _PyObject_VectorcallTstate(tstate, lz->func, stack, nargs, NULL);
exit:
// 調用完畢之后, 將stack中指針指向的對象的引用計數減1
for (Py_ssize_t i=0; i < nargs; i++) {
Py_DECREF(stack[i]);
}
// 不相等的話, 說明該stack是在堆區申請的, 要釋放
if (stack != small_stack) {
PyMem_Free(stack);
}
// 返回result
return result;
}
然后突然發現map對象還有一個鮮為人知的一個方法,也是一個沒有什么卵用的方法。說來慚愧,要不是看源碼,我還真沒注意過。
static PyObject *
map_reduce(mapobject *lz, PyObject *Py_UNUSED(ignored))
{
// 獲取迭代器的元素個數
Py_ssize_t numargs = PyTuple_GET_SIZE(lz->iters);
// 申請一個元素, 空間是numargs + 1 個
PyObject *args = PyTuple_New(numargs+1);
Py_ssize_t i;
if (args == NULL)
return NULL;
Py_INCREF(lz->func);
// 將函數設置為args的第一個元素
PyTuple_SET_ITEM(args, 0, lz->func);
// 然后再將剩下的迭代器也設置在args中
for (i = 0; i<numargs; i++){
PyObject *it = PyTuple_GET_ITEM(lz->iters, i);
Py_INCREF(it);
PyTuple_SET_ITEM(args, i+1, it);
}
// 將 Py_TYPE(lz) 和 args 打包成一個元組返回
// 所以從結果上看, 返回的內容應該是: ( <class 'map'>, (函數, 迭代器1, 迭代器2, 迭代器3, ......) )
return Py_BuildValue("ON", Py_TYPE(lz), args);
}
static PyMethodDef map_methods[] = {
// 然后這個函數叫 __reduce__
{"__reduce__", (PyCFunction)map_reduce, METH_NOARGS, reduce_doc},
{NULL, NULL} /* sentinel */
};
然后我們來演示一下:
from pprint import pprint
m = map(lambda x, y, z: x + y + z, [1, 2, 3], [2, 3, 4], [3, 4, 5])
pprint(m.__reduce__())
"""
(<class 'map'>,
(<function <lambda> at 0x000001D2791451F0>,
<list_iterator object at 0x000001D279348640>,
<list_iterator object at 0x000001D279238700>,
<list_iterator object at 0x000001D27950AF40>))
"""
filter底層實現
然后我們filter的實現原理,看完了map之后,再看filter就簡單許多了。
lst = [1, 2, 3, 4, 5]
print(list(filter(lambda x: x % 2 !=0, lst))) # [1, 3, 5]
首先filter接收兩個元素,第一個參數是一個函數(類、方法),第二個參數是一個可迭代對象。然后當我們迭代的時候會將可迭代對象中每一個元素都傳入到函數中,如果返回的結果為真,則保留;為假,則丟棄。
但是,其實第一個參數除了是一個可調用的對象之外,它還可以是None。
lst = ["夏色祭", "", [], 123, 0, {}, [1]]
# 會自動選擇結果為真的元素
print(list(filter(None, lst))) # ['夏色祭', 123, [1]]
至於為什么,一會看源碼filter的實現就清楚了。
下面看看底層結構:
typedef struct {
PyObject_HEAD
PyObject *func;
PyObject *it;
} filterobject;
我們看到和map對象是一致的,沒有什么區別。因為map、filter都不會立刻調用,而是返回一個相應的對象。
static PyObject *
filter_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyObject *func, *seq; // 函數、可迭代對象
PyObject *it; // 可迭代對象的迭代器
filterobject *lz; // 返回值, filter對象
// filter也不接受關鍵字參數
if (type == &PyFilter_Type && !_PyArg_NoKeywords("filter", kwds))
return NULL;
// 只接受兩個參數
if (!PyArg_UnpackTuple(args, "filter", 2, 2, &func, &seq))
return NULL;
// 獲取seq對應的迭代器
it = PyObject_GetIter(seq);
if (it == NULL)
return NULL;
// 為filter對象申請空間
lz = (filterobject *)type->tp_alloc(type, 0);
if (lz == NULL) {
Py_DECREF(it);
return NULL;
}
// 增加函數的引用計數
Py_INCREF(func);
// 初始化成員
lz->func = func;
lz->it = it;
// 返回
return (PyObject *)lz;
}
和map是類似的,因為本質上它們做的事情都是差不多的,下面看看迭代過程。
static PyObject *
filter_next(filterobject *lz)
{
PyObject *item; // 迭代器中迭代出來的每一個元素
PyObject *it = lz->it; // 迭代器
long ok; // 是否為真, 1表示真、0表示假
PyObject *(*iternext)(PyObject *); // 函數指針, 接收一個PyObject *, 返回一個PyObject *
// 如果 func == None 或者 func == bool, 那么checktrue為真; 會走單獨的方法, 所以給func傳遞一個None是完全合法的
int checktrue = lz->func == Py_None || lz->func == (PyObject *)&PyBool_Type;
// 迭代器的 __next__ 方法
iternext = *Py_TYPE(it)->tp_iternext;
// 無限循環
for (;;) {
// 迭代出迭代器的每一個元素
item = iternext(it);
if (item == NULL)
return NULL;
// 如果checkture, 或者說如果func == None || func == bool
if (checktrue) {
// PyObject_IsTrue(item)實際上就是在判斷item是否為真, 像0、長度為0的序列、False、None為假
// 另外我們在if語句的時候經常會寫 if item: 這種形式, 但是很少會寫 if bool(item):
// 因為bool(item)底層也是調用 PyObject_IsTrue
// 而if item: 如果你查看它的字節碼的話, 會發現有這么一條指令: POP_JUMP_IF_FALSE
// 它在底層也是調用了 PyObject_IsTrue, 因此完全沒有必要寫成 if bool(item): 這種形式
ok = PyObject_IsTrue(item);
// 而如果func為None或者bool的話, 那么直接走PyObject_IsTrue
} else {
// 否則的話, 會調用我們傳遞的func
// 這里的 good 就是函數調用的返回值
PyObject *good;
// 調用函數, 將返回值賦值給good
good = PyObject_CallFunctionObjArgs(lz->func, item, NULL);
// 如果 good 等於 NULL, 說明函數調用失敗; 說句實話, 源碼中做的很多異常捕獲都是針對解釋器內部的
// 尤其像底層這種和NULL進行比較的, 我們在使用Python的時候, 很少會出現
if (good == NULL) {
Py_DECREF(item);
return NULL;
}
// 判斷 good 是否為真
ok = PyObject_IsTrue(good);
Py_DECREF(good); // 減少其引用計數, 因為它不被外界所使用
}
// 如果ok大於0, 說明將item傳給函數調用之后返回的結果為真, 那么將item返回
if (ok > 0)
return item;
// 同時減少其引用計數
// 如果等於0, 說明為假, 那么進行下一輪循環
Py_DECREF(item);
// 小於0的話, 表示PyObject_IsTrue調用失敗了, 調用失敗會返回-1
// 但還是那句話, 這種情況, 在Python的使用層面上幾乎不可能發生
if (ok < 0)
return NULL;
}
}
所以看到這里你還覺得Python神秘嗎,從源代碼層面我們看的非常流暢,只要你有一定的C語言基礎即可。還是那句話,盡管我們不可能寫一個解釋器,因為背后涉及的東西太多了,但至少我們在看的過程中,很清楚底層到底在做什么。而且這背后的實現,如果讓你設計一個方案的話,那么相信你也一樣可以做到。
還是拿關鍵字 in 舉例子,像"b" in ["a", "b", "c"]
我們知道結果為真。那如果讓你設計關鍵字 in 的實現,你會怎么做呢?反正基本上都會想到,遍歷 in 后面的可迭代對象唄,將里面的元素 依次和 in前面的元素進行比較,如果出現相等的,返回真;遍歷完了也沒發現相等的,那么返回假。如果你是這么想的,那么恭喜你,Python解釋器內部也是這么做的,我們以列表為例:
// item in 列表: 本質上就是調用 list.__contains__(列表, item) 或者 列表.__contains__(item)
static int
list_contains(PyListObject *a, PyObject *el)
{
PyObject *item; // 列表中的每一個元素
Py_ssize_t i; // 循環變量
int cmp; // 比較的結果
// cmp初始為0
for (i = 0, cmp = 0 ; cmp == 0 && i < Py_SIZE(a); ++i) {
// 獲取PyListObject中的每一個元素
item = PyList_GET_ITEM(a, i);
Py_INCREF(item);
// 調用PyObject_RichCompareBool進行比較, 大於、小於、不等於之類的都是使用這個函數, 具體是哪一種則通過第三個參數控制
// 而前兩個元素則是比較的對象
cmp = PyObject_RichCompareBool(el, item, Py_EQ);
Py_DECREF(item);
}
// 如果出現相等的元素, 那么cmp為1, 因此cmp == 0 && i < Py_SIZE(a)會不成立, 直接結束循環
// 如果沒有出現相等的元素, 那么會一直遍歷整個列表, 始終沒有出現相等的元素, 那么cmp還是0
// 為1代表真, 為0代表假
return cmp;
}
以上便是關鍵字 in,是不是很簡單呢?所以個人推薦沒事的話可以多讀一讀Python解釋器,如果你不使用Python / C API進行編程的話,那么不需要你有太高的C語言水平(況且現在還有Cython)。如果你想寫出高質量、並且精簡利落的Python代碼,那么就勢必要懂得背后的實現原理。比如:我們看幾道思考題,自己亂編的。
1. 為什么 方法一 要比 方法二 更高效?
lst = [1, 2, 3, 4, 5]
# 方法一
def f1():
return [f"item: {item}" for item in lst]
# 方法二
def f2():
res = []
for item in lst:
res.append(f"item: {item}")
return res
所以這道題考察的實際上是列表解析為什么更快?首先Python中的變量在底層本質上都是一個泛型指針PyObject *,調用res.append的時候實際上會進行一次屬性查找。會調用
PyObject_GetAttr(res, "append")
,去尋找該對象是否有 append 函數,如果有的話,那么進行獲取然后調用;而列表解析,Python在編譯的時候看到左右的兩個中括號就知道這是一個列表解析式,所以它會立刻知道自己該干什么,會直接調用C一級函數 PyList_Append,因為Python對這些內置對象非常熟悉。所以列表解析少了一層屬性查找的過程,因此它的效率會更高。
2. 假設有三個變量a、b、c,三個常量 "xxx"、123、3.14,我們要判斷這三個變量對應的值 和 這三個常量是否相等,該怎么做呢?注意:順序沒有要求,可以是 a == "xxx"、也可以是 b == "xxx",只要這個三個變量對應的值正好也是 "xxx"、123、3.14 就行。
顯然最方便的是使用集合:
a, b, c = 3.14, "xxx", 123
print(not {a, b, c} - {"xxx", 3.14, 123}) # True
3. 令人困惑的生成器解析式,請問下面哪段代碼會報錯?
# 代碼一
x = ("xxx" for _ in dasdasdad)
# 代碼二
x = (dasdasdad for _ in "xxx")
首先生成器解析式,只有在執行的時候才會真正的產出值。但是關鍵字 in 后面的變量是會提前確定的,所以代碼一會報錯,拋出 NameError;但代碼二不會,因為只有在產出值的時候才會去尋找變量 dasdasdad 指向的值。
再留個兩個思考題,為什么會出現這種結果呢?
# 思考題一:
class A:
x = 123
print(x)
lst = [x for _ in range(3)]
"""
123
NameError: name 'x' is not defined
"""
########################################################################
# 思考題二:
def f():
a = 123
print(eval("a"))
print([eval("a") for _ in range(3)])
f()
"""
123
NameError: name 'a' is not defined
"""
像這樣類似的問題還有很多很多,當然最關鍵的還是理解底層的數據結構 以及 解釋器背后的執行原理,只有這樣才能寫出更加高效的代碼。
回到正題,filter 也有 __reduce__ 方法,和 map 類似。
f = filter(None, [1, 2, 3, 0, "", [], "xx"])
print(f.__reduce__()) # (<class 'filter'>, (None, <list_iterator object at 0x00000239AF2AB0D0>))
print(list(f.__reduce__()[1][1])) # [1, 2, 3, 0, '', [], 'xx']
zip底層實現
最后看看 zip,其實 zip 和 map 也是有着高度相似之處的,首先它們都可以接受任意個可迭代對象。而且 zip,我們完全可以使用 map 來進行模擬。
print(
list(zip([1, 2, 3], [11, 22, 33], [111, 222, 333]))
) # [(1, 11, 111), (2, 22, 222), (3, 33, 333)]
print(
list(map(lambda x, y, z: (x, y, z), [1, 2, 3], [11, 22, 33], [111, 222, 333]))
) # [(1, 11, 111), (2, 22, 222), (3, 33, 333)]
print(
list(map(lambda *args: args, [1, 2, 3], [11, 22, 33], [111, 222, 333]))
) # [(1, 11, 111), (2, 22, 222), (3, 33, 333)]
# 所以我們看到實現zip, 完全可以使用 map, 只需要多指定一個函數即可
所以 zip 的底層實現同樣很簡單,我們來看一下:
typedef struct {
PyObject_HEAD
Py_ssize_t tuplesize;
PyObject *ittuple;
PyObject *result;
} zipobject;
// 以上便是zip對象的底層定義, 這些字段的含義, 我們暫時先不討論, 它們會體現在zip_new方法中, 我們到時候再說
目前我們根據結構體里面的成員,可以得到一個 zipobject 顯然是占 40 字節的,16 + 8 + 8 + 8,那么結果是不是這樣呢?我們來試一下就知道了。
z1 = zip([1, 2, 3], [11, 22, 33])
z2 = zip([1, 2, 3, 4], [11, 22, 33, 44])
z3 = zip([1, 2, 3], [11, 22, 33], [111, 222, 333])
print(z1.__sizeof__()) # 40
print(z2.__sizeof__()) # 40
print(z3.__sizeof__()) # 40
所以我們分析的沒有錯,任何一個 zip 對象所占的大小都是 40 字節。所以在計算內存大小的時候,有人會好奇這到底是怎么計算的,其實就是根據底層的結構體進行計算的。
注意:如果你使用 sys.getsizeof 函數計算的話,可能會多出 16 個字節,這是因為對於可變對象,它們是會被 GC 跟蹤的。在創建的時候,它們會被掛到零代鏈表中,所以它們額外還會有一個 前繼指針 和 一個 后繼指針,而 sys.getsizeof 將這兩個指針的大小也算在內了。
下面看看 zip 對象是如何被實例化的。
static PyObject *
zip_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
zipobject *lz; // zip 對象
Py_ssize_t i; // 循環變量
PyObject *ittuple; // 所有可迭代對象的迭代器組成的元組
PyObject *result; // "代碼中有體現"
Py_ssize_t tuplesize; // 可迭代對象的數量
// zip同樣不需要關鍵字參數, 但是在3.10的時候將會提供一個關鍵字參數strict, 如果為True, 表示可迭代對象之間的長度必須相等, 否則報錯
// strict如果為False, 則和目前是等價的, 會自動以短的為准
if (type == &PyZip_Type && !_PyArg_NoKeywords("zip", kwds))
return NULL;
// args必須使用一個PyTupleObject *
assert(PyTuple_Check(args));
// 獲取可迭代對象的數量
tuplesize = PyTuple_GET_SIZE(args);
// 申請一個元組, 長度為tuplesize, 用於存放可迭代對象對應的迭代器
ittuple = PyTuple_New(tuplesize);
if (ittuple == NULL) // 為NULL表示申請失敗
return NULL;
// 然后依次遍歷
for (i=0; i < tuplesize; ++i) {
// 獲取傳遞的可迭代對象
PyObject *item = PyTuple_GET_ITEM(args, i);
// 通過PyObject_GetIter獲取對應的迭代器
PyObject *it = PyObject_GetIter(item);
if (it == NULL) {
// 為NULL表示獲取失敗, 減少ittuple的引用計數, 返回NULL
Py_DECREF(ittuple);
return NULL;
}
// 設置在ittuple中
PyTuple_SET_ITEM(ittuple, i, it);
}
// 這里又申請一個元組result, 長度也為tuplesize
result = PyTuple_New(tuplesize);
if (result == NULL) {
Py_DECREF(ittuple);
return NULL;
}
// 然后將內部的所有元素都設置為None, Py_None就是Python中的None
for (i=0 ; i < tuplesize ; i++) {
Py_INCREF(Py_None);
PyTuple_SET_ITEM(result, i, Py_None);
}
// 申請一個zip對象
lz = (zipobject *)type->tp_alloc(type, 0);
// 申請失敗減少引用計數, 返回NULL
if (lz == NULL) {
Py_DECREF(ittuple);
Py_DECREF(result);
return NULL;
}
// 初始化成員
lz->ittuple = ittuple;
lz->tuplesize = tuplesize;
lz->result = result;
// 轉成泛型指針PyObject *之后返回
return (PyObject *)lz;
}
再來看看,zip對象的定義:
typedef struct {
PyObject_HEAD
Py_ssize_t tuplesize;
PyObject *ittuple;
PyObject *result;
} zipobject;
如果以:zip([1, 2, 3], [11, 22, 33], [111, 222, 333])
為例的話,那么:
tuplesize: 3
ittuple: ([1, 2, 3].__iter__(), [11, 22, 33].__iter__(), [111, 222, 333].__iter__())
result: (None, None, None)
所以目前來說,其它的很好理解,唯獨這個result讓人有點懵,搞不懂它是干什么的。不過既然有這個成員,那就說明它肯定有用武之地,而派上用場的地方不用想,肯定是在迭代的時候使用。
static PyObject *
zip_next(zipobject *lz)
{
Py_ssize_t i; // 循環遍變量
Py_ssize_t tuplesize = lz->tuplesize; // 可迭代對象數量
PyObject *result = lz->result; // (None, None, ....)
PyObject *it; // 每一個迭代器
// 代碼中體現
PyObject *item;
PyObject *olditem;
// tuplesize == 0, 直接返回
if (tuplesize == 0)
return NULL;
// 如果 result 的引用計數為1, 證明該元組的空間的被申請了
if (Py_REFCNT(result) == 1) {
// 因為它要作為返回值返回, 引用計數加1
Py_INCREF(result);
// 遍歷
for (i=0 ; i < tuplesize ; i++) {
// 依次獲取每一個迭代器
it = PyTuple_GET_ITEM(lz->ittuple, i);
// 迭代出相應的元素
item = (*Py_TYPE(it)->tp_iternext)(it);
// 如果出現了NULL, 證明迭代結束了, 會直接停止
// 所以會以元素最少的可迭代對象(迭代器)為准
if (item == NULL) {
Py_DECREF(result);
return NULL;
}
// 設置在 result 中, 但是要先獲取result中原來的元素, 並將其引用計數減1, 因為元組不再持有對它的引用
olditem = PyTuple_GET_ITEM(result, i);
PyTuple_SET_ITEM(result, i, item);
Py_DECREF(olditem);
}
} else {
// 否則的話同樣的邏輯, 只不過需要自己重新手動申請一個tuple
result = PyTuple_New(tuplesize);
if (result == NULL)
return NULL;
// 然后下面的邏輯是類似的
for (i=0 ; i < tuplesize ; i++) {
it = PyTuple_GET_ITEM(lz->ittuple, i);
item = (*Py_TYPE(it)->tp_iternext)(it);
if (item == NULL) {
Py_DECREF(result);
return NULL;
}
PyTuple_SET_ITEM(result, i, item);
}
}
// 返回元組 result
return result;
}
所以當我們進行迭代的時候,迭代出來的是一個元組。
z = zip([1, 2, 3], [11, 22, 33])
print(z.__next__()) # (1, 11)
# 即使只有一個可迭代對象, 依舊是一個元組, 因為底層返回的result就是一個元組
z = zip([1, 2, 3])
print(z.__next__()) # (1,)
# 可迭代對象的嵌套也是一樣的規律, 直接把里面的列表看成一個標量即可
z = zip([[1, 2, 3], [11, 22, 33]])
print(z.__next__()) # ([1, 2, 3],)
最后,zip 也有一個__reduce__ 方法:
z = zip([1, 2, 3], [11, 22, 33])
print(z.__reduce__())
# (<class 'zip'>, (<list_iterator object at 0x0000018D1723B0D0>, <list_iterator object at 0x0000018D1723B040>))
print([tuple(_) for _ in z.__reduce__()[1]]) # [(1, 2, 3), (11, 22, 33)]
map、filter 和 列表解析之間的區別
其實在使用 map、filter 的時候,我們完全可以使用列表解析來實現。比如:
lst = [1, 2, 3, 4]
print([str(_) for _ in lst]) # ['1', '2', '3', '4']
print(list(map(str, lst))) # ['1', '2', '3', '4']
這兩者之間實際上是沒有什么太大區別的,都是將 lst 中的元素一個一個迭代出來、然后調用 str 、返回結果。如果非要找出區別話,就是列表解析使用的是 Python 的for循環,而調用list的時候使用的是C中的for循環。從這個角度來說,使用 map 的效率會更高一些。
所以后者的效率稍微更高一些,因為列表解析用的是 Python 的for循環,list(map(func, iter))
用的是C的for循環。但是注意:如果是下面這種做法的話,會得到相反的結果。
我們看到 map 貌似變慢了,其實原因很簡單,后者多了一層匿名函數的調用,所以速度變慢了。
如果列表解析也是函數調用的話:
會發現速度更慢了,當然這種做法完全是吃飽了撐的。之所以說這些,是想說明在同等條件下,list(map) 這種形式是要比列表解析快的。當然在工作中,這兩者都是可以使用的,這點效率上的差別其實不用太在意,如果真的到了需要在乎這點差別的時候,那么你應該考慮的是換一門更有效率的靜態語言。
filter 和 列表解析之間的差別,也是如此。
對於過濾含有 1000個 False 和 1個True 的元組,它們的結果都是一樣的,但是誰的效率更高呢?首先第一種方式 肯定比 第二種方式快,因為第二種方式涉及到函數的調用;但是第三種方式,我們知道它在底層會走單獨的分支,所以再加上之前的結論,我們認為第三種方式是最快的。
結果也確實是我們分析的這樣,當然我們說在底層 None 和 bool 都會走相同的分支,所以這里將 None 換成 bool 也是可以的。雖然 bool 是一個類,但是通過 filter_new 函數我們知道,底層不會進行調用,也是直接使用 PyObject_IsTrue,可以將 None 換成 bool 看看結果如何,應該是差不多的。
總結
所以 map、filter 完全可以使用列表解析替代,如果執行的邏輯比較復雜的話,那么對於 map、filter 而言就要寫匿名函數了。但邏輯簡單的話,比如:獲取為真的元素,完全可以通過list(filter(None, lst))
實現,效率更高,因為它走的是相當於是C的循環;但如果獲取大於3的元素,那么就需要使用list(filter(lambda x: x > 3, lst))
這種形式了,而我們說它的效率是不如列表解析[x for x in lst if x > 3]
的,因為前者多了一層函數調用。
但是在工作中,這兩種方式都是可以的,使用哪一種就看個人喜好。到此我們發現,如果排除那一點點效率上的差異,那么確實有列表解析式就完全足夠了,因為列表解析式可以同時實現 map、filter 的功能,而且表達上也更加地直觀。只不過是 map、filter 先出現,然后才有的列表解析式,但是前者依舊被保留了下來。
當然 map、filter 返回的是一個可迭代對象,它不會立即計算,可以節省資源;當然這個功能,我們也可以通過生成器表達式來實現。
map、filter、zip 的底層實現我們就看完了,是不是很簡單呢?
另外,如果你得到的結論和我這里的不一致,那么不妨把可迭代對象的元素個數設置的稍微大一些,最終結論和我這里一定是一樣的。