<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Python 一切皆物件,型別物件定義了哪些操作,決定了範例物件擁有哪些行為。
比如型別物件如果定義了 __iter__,那麼其範例物件便被稱為可迭代物件(iterable),像字串、元組、列表、字典、集合等等都是可迭代物件。而整數、浮點數,由於其型別物件沒有定義 __iter__,所以它們不是可迭代物件。
from typing import Iterable print( isinstance("", Iterable), isinstance((), Iterable), isinstance([], Iterable), isinstance({}, Iterable), isinstance(set(), Iterable), ) # True True True True True print( isinstance(0, Iterable), isinstance(0.0, Iterable), ) # False False
可迭代物件的一大特點就是它可以使用 for 迴圈進行遍歷,但是能被 for 迴圈遍歷的則不一定是可迭代物件。我們舉個例子:
class A: def __getitem__(self, item): return f"引數item: {item}" a = A() # 內部定義了 __getitem__ # 首先可以讓範例物件像字典一樣存取屬性 print(a["name"]) # 引數item: name print(a["satori"]) # 引數item: satori # 此外還可以像可迭代物件一樣被 for 迴圈 # 迴圈的時候會自動給 item 傳值,0 1 2 3... # 如果內部出現了 StopIteration,迴圈結束 # 否則會一直迴圈下去。這裡我們手動 break for idx, val in enumerate(a): print(val) if idx == 5: break """ 引數item: 0 引數item: 1 引數item: 2 引數item: 3 引數item: 4 引數item: 5 """
所以實現了__getitem__的類的範例,也是可以被for迴圈的,但它並不是可迭代物件。
from typing import Iterable print(isinstance(a, Iterable)) # False
總之判斷一個物件是否是可迭代物件,就看它的型別物件有沒有實現 __iter__。
可迭代物件我們知道了,那什麼是迭代器呢?很簡單,呼叫可迭代物件的 __iter__ 方法,得到的就是迭代器。
不同型別的物件,都有自己的迭代器,舉個例子。
lst = [1, 2, 3] # 底層呼叫的其實是 list.__iter__(lst) # 從 C 的角度上看,就是 PyList_Type.tp_iter(lst) it = lst.__iter__() print(it) # <list_iterator object at 0x000001DC6E898640> print( str.__iter__("") ) # <str_iterator object at 0x000001DC911B8070> print( tuple.__iter__(()) ) # <tuple_iterator object at 0x000001DC911B8070>
迭代器也是可迭代物件,只不過迭代器內部的 __iter__ 返回的還是它本身。當然啦,在建立迭代器的時候,我們更常用內建函數 iter。
lst = [1, 2, 3] # 等價於 type(lst).__iter__(lst) it = iter(lst)
但是 iter 函數還有一個鮮為人知的用法,我們來看一下:
val = 0 def foo(): global val val += 1 return val # iter 可以接收一個引數: iter(可迭代物件) # iter 也可以接收兩個引數: iter(可呼叫物件, value) for i in iter(foo, 5): print(i) """ 1 2 3 4 """
如果接收的是兩個引數,那麼第一個引數一定是 callable。進行迭代的時候,會不停地呼叫接收的可呼叫物件,每次迭代出來的值便是 callable 的返回值。當返回值等於傳遞第二個引數 value(在底層被稱為哨兵)時,終止迭代。我們看一下 iter 函數的底層實現。
static PyObject * builtin_iter(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { PyObject *v; // iter 函數要麼接收一個引數, 要麼接收兩個引數 if (!_PyArg_CheckPositional("iter", nargs, 1, 2)) return NULL; v = args[0]; // 如果接收一個引數 // 那麼直接使用 PyObject_GetIter 獲取對應的迭代器即可 // 可迭代物件的型別不同,那麼得到的迭代器也不同 if (nargs == 1) return PyObject_GetIter(v); // 如果接收的不是一個引數, 那麼一定是兩個引數 // 如果是兩個引數, 那麼第一個引數一定是可呼叫物件 if (!PyCallable_Check(v)) { PyErr_SetString(PyExc_TypeError, "iter(v, w): v must be callable"); return NULL; } // 獲取value(哨兵) PyObject *sentinel = args[1]; //呼叫PyCallIter_New //得到 calliterobject 物件 /* 該物件位於 Objects/iterobject.c 中 */ return PyCallIter_New(v, sentinel); }
以上就是 iter 函數的內部邏輯,既可以接收一個引數,也可以接收兩個引數。這裡我們只看接收一個可迭代物件的情況,所以核心就在於 PyObject_GetIter,它是根據可迭代物件生成迭代器的關鍵,我們來看一下它的邏輯是怎麼樣的?該函數定義在 Objects/abstract.c 中。
PyObject * PyObject_GetIter(PyObject *o) { // 獲取可迭代物件的型別物件 PyTypeObject *t = Py_TYPE(o); // 我們說型別物件定義的操作,決定了範例物件的行為 // 範例物件呼叫的那些方法都是定義在型別物件裡面的 // 所以obj.func()本質上就是type(obj).func(obj)的語法糖 getiterfunc f; // 所以這裡是獲取型別物件的 tp_iter 成員 // 也就是 Python 中的 __iter__ f = t->tp_iter; // 如果 f 為 NULL // 說明該型別物件內部的tp_iter成員被初始化為NULL // 即內部沒有定義 __iter__ // 像str、tuple、list等型別物件,它們的tp_iter成員都是不為NULL的 if (f == NULL) { // 如果 tp_iter 為 NULL,那麼直譯器會退而求其次 // 檢測該型別物件中是否定義了 __getitem__ // 如果定義了,那麼直接呼叫PySeqIter_New // 得到一個seqiterobject物件 // 下面的PySequence_Check負責檢測型別物件是否實現了__getitem__ if (PySequence_Check(o)) return PySeqIter_New(o); // 走到這裡說明該型別物件既沒有__iter__、也沒有__getitem__ // 因此它的範例物件不具備可迭代的性質,於是丟擲異常 return type_error("'%.200s' object is not iterable", o); } else { // 否則說明定義了__iter__,於是直接進行呼叫 // Py_TYPE(o)->tp_iter(o) 返回對應的迭代器 PyObject *res = (*f)(o); // 但如果返回值res不為NULL、並且還不是迭代器 // 證明 __iter__ 的返回值有問題,於是丟擲異常 if (res != NULL && !PyIter_Check(res)) { PyErr_Format(PyExc_TypeError, "iter() returned non-iterator " "of type '%.100s'", Py_TYPE(res)->tp_name); Py_DECREF(res); res = NULL; } // 返回 res return res; } }
所以我們看到這便是 iter 函數的底層實現,並且當型別物件內部沒有定義 __iter__ 時,直譯器會退而求其次檢測內部是否定義了 __getitem__。
因此以上就是迭代器的建立過程,每個可迭代物件都有自己的迭代器,而迭代器本質上只是對原始資料的一層封裝罷了。
由於迭代器的種類非常多,字串、元組、列表等等,都有自己的迭代器,這裡就不一一介紹了。我們就以列表的迭代器為例,看看迭代器在底層的結構是怎麼樣的。
typedef struct { PyObject_HEAD Py_ssize_t it_index; //指向建立該迭代器的列表 PyListObject *it_seq; } listiterobject;
顯然對於列表而言,迭代器就是在其之上進行了一層簡單的封裝,所謂元素迭代本質上還是基於索引,並且我們每迭代一次,索引就自增 1。一旦出現索引越界,就將 it_seq 設定為 NULL,表示迭代器迭代完畢。
我們實際演示一下:
from ctypes import * class PyObject(Structure): _fields_ = [ ("ob_refcnt", c_ssize_t), ("ob_size", c_void_p) ] class ListIterObject(PyObject): _fields_ = [ ("it_index", c_ssize_t), ("it_seq", POINTER(PyObject)) ] it = iter([1, 2, 3]) it_obj = ListIterObject.from_address(id(it)) # 初始的時候,索引為0 print(it_obj.it_index) # 0 # 進行迭代 next(it) # 索引自增1,此時it_index等於1 print(it_obj.it_index) # 1 # 再次迭代 next(it) # 此時it_index等於2 print(it_obj.it_index) # 2 # 再次迭代 next(it) # 此時it_index等於3 print(it_obj.it_index) # 3
當 it_index 為 3 的時候,如果再次迭代,那麼底層發現 it_index 已超過最大索引,就知道迭代器已經迭代完畢了。然後會將 it_seq 設定為 NULL,並丟擲 StopIteration。如果是 for 迴圈,那麼會自動捕獲此異常,然後停止迴圈。
所以這就是迭代器,真的沒有想象中的那麼神祕,甚至在知道它的實現原理之後,還覺得有點 low。
就是將原始的封包了一層,加了一個索引而已。所謂的迭代仍然是基於索引來做的,並且每迭代一次,索引自增 1。當索引超出範圍時,證明迭代完畢了,於是將 it_seq 設定為 NULL,丟擲 StopIteration。
我們知道在迭代元素的時候,可以通過 next 內建函數,當然它本質上也是呼叫了物件的 __next__ 方法。
static PyObject * builtin_next(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { PyObject *it, *res; // 同樣接收一個引數或者兩個引數 // 因為呼叫next函數時,可以傳入一個預設值 // 當迭代器沒有元素可以迭代的時候,會返回指定的預設值 if (!_PyArg_CheckPositional("next", nargs, 1, 2)) return NULL; it = args[0]; // 第一個引數必須是一個迭代器 if (!PyIter_Check(it)) { // 否則的話, 丟擲TypeError // 表示第一個引數傳遞的不是一個迭代器 PyErr_Format(PyExc_TypeError, "'%.200s' object is not an iterator", it->ob_type->tp_name); return NULL; } // it->ob_type 表示獲取型別物件,也就是該迭代器的型別 // 可能是列表的迭代器、元組的迭代器、字串的迭代器等等 // 具體是哪一種不重要,因為實現了多型 // 然後再獲取 tp_iternext 成員,相當於__next__ // 拿到函數指標之後,傳入迭代器進行呼叫 res = (*it->ob_type->tp_iternext)(it); // 如果 res 不為 NULL, 那麼證明迭代到值了, 直接返回 if (res != NULL) { return res; } else if (nargs > 1) { // 否則的話,說明 res == NULL, // 這意味著迭代完畢了,或者程式出錯了 // 然後看 nargs 是否大於1, 如果大於1, 說明設定了預設值 PyObject *def = args[1]; // 如果出現異常 if (PyErr_Occurred()) { // 那麼就看該異常的種類 // 能否匹配 StopIteration if(!PyErr_ExceptionMatches(PyExc_StopIteration)) // 如果不是,說明程式的邏輯有問題 // 於是直接return NULL,結束執行 // 然後在 Python 裡面我們會看到列印到stderr中的異常資訊 return NULL; // 如果是 StopIteration,證明迭代完畢了 // 但我們設定了預設值,那麼就應該返回預設值 // 而不應該丟擲 StopIteration,於是將異常回溯棧給清空 PyErr_Clear(); } // 然後增加預設值的參照計數, 並返回 Py_INCREF(def); return def; } else if (PyErr_Occurred()) { //走到這裡說明程式出異常了,並且沒有指定預設值 //那麼這種情況,不管什麼異常都直接丟擲 return NULL; } else { // 都不是的話,仍是直接丟擲 StopIteration PyErr_SetNone(PyExc_StopIteration); return NULL; } }
以上就是next函數的背後邏輯,實際上還是呼叫了迭代器的__next__方法。
lst = [1, 2, 3] it = iter(lst) # 然後迭代,等價於next(it) print(type(it).__next__(it)) # 1 print(type(it).__next__(it)) # 2 print(type(it).__next__(it)) # 3 # 但是next可以指定預設值 # 如果不指定預設值,或者還是type(it).__next__(it) # 那麼就會報錯,會丟擲StopIteration print(next(it, 666)) # 666
以上就是元素的迭代,但是我們知道內建函數next要更強大一些,因為它還可以指定一個預設值。當然在不指定預設值的情況下,next(it) 和 type(it).__next__(it) 是等價的。
我們仍以列表的迭代器為例,看看 __next__ 的具體實現。
static PyObject * listiter_next(listiterobject *it) { PyListObject *seq; //列表 PyObject *item; //元素 assert(it != NULL); //拿到具體對應的列表 seq = it->it_seq; //如果seq為NULL,證明迭代器已經迭代完畢 //否則它不會為NULL if (seq == NULL) return NULL; assert(PyList_Check(seq)); //如果索引小於列表的長度,證明尚未迭代完畢 if (it->it_index < PyList_GET_SIZE(seq)) { //通過索引獲取指定元素 item = PyList_GET_ITEM(seq, it->it_index); //it_index自增1 ++it->it_index; //增加參照計數後返回 Py_INCREF(item); return item; } //否則的話,說明此次索引正好已經超出最大範圍 //意味著迭代完畢了,將it_seq設定為NULL //並減少它的參照計數,然後返回 it->it_seq = NULL; Py_DECREF(seq); return NULL; }
顯然這和我們之前分析的是一樣的,以上我們就以列表為例,考察了迭代器的實現原理和元素迭代的具體過程。當然其它物件也有自己的迭代器,有興趣可以自己看一看。
到此,我們再次體會到了 Python 的設計哲學,通過 PyObject * 和 ob_type 實現了多型。原因就在於它們接收的不是物件本身,而是物件的 PyObject * 泛型指標。
不管變數 obj 指向什麼樣的可迭代物件,都可以交給 iter 函數,會呼叫型別物件內部的 __iter__,底層是 tp_iter,得到對應的迭代器。不管變數 it 指向什麼樣的迭代器,都可以交給 next 函數進行迭代,會呼叫迭代器的型別物件的 __next__,底層是 tp_iternext,將值迭代出來。
至於__iter__和__next__本身,每個迭代器都會有,我們這裡只以列表的迭代器為例。
到此這篇關於一文帶你解密Python迭代器的實現原理的文章就介紹到這了,更多相關Python迭代器內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45