網站首頁 編程語言 正文
前言:
在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
打印的結果是 False。
總之判斷一個對象是否是可迭代對象,就看它的類型對象有沒有實現__iter__。可迭代對象我們知道了,那什么是迭代器呢?很簡單,調用可迭代對象的__iter__方法,得到的就是迭代器。
迭代器的創建
不同類型的對象,都有自己的迭代器,舉個栗子:
lst = [1, 2, 3]
#底層調用的其實是list.__iter__(lst)
#或者說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
"""
進行迭代的時候,會不停地調用接收的可調用對象,直到返回值等于傳遞第二個參數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 中
typedef struct {
PyObject_HEAD
PyObject *it_callable;
PyObject *it_sentinel;
} calliterobject;
*/
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__
//__getitem__ 對應 tp_as_sequence->sq_item
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 函數的底層實現,但是里面提到了__getitem__。我們說如果類型對象內部沒有定義 __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))
// 如果不是,說明Python程序的邏輯有問題
// 于是直接return NULL,結束執行
// 然后在 Python 里面我們會看到打印到stderr中的異常信息
return NULL;
// 如果是 StopIteration,證明迭代完畢了
// 但我們設置了默認值,那么就應該返回默認值
// 而不應該拋出 StopIteration,于是將異常回溯棧給清空
PyErr_Clear();
}
// 然后增加默認值的引用計數, 并返回
Py_INCREF(def);
return def;
} else if (PyErr_Occurred()) {
//走到這里說明 res == NULL,并且沒有指定默認值
//那么當發生異常時,將異常直接拋出
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__的具體實現。但是要想找到具體實現,首先要找到它的類型對象。
//迭代器的類型對象
PyTypeObject PyListIter_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"list_iterator", /* tp_name */
sizeof(listiterobject), /* tp_basicsize */
0, /* tp_itemsize */
/* methods */
(destructor)listiter_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
0, /* tp_doc */
(traverseproc)listiter_traverse, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
PyObject_SelfIter, /* tp_iter */
(iternextfunc)listiter_next, /* tp_iternext */
listiter_methods, /* tp_methods */
0, /* tp_members */
};
我們看到它的tp_iternext成員指向了listiter_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的設計哲學,變量只是一個指針,傳遞變量的時候相當于傳遞指針(將指針拷貝一份),但是操作一個變量的時候會自動操作變量(指針)指向的內存。
比如:a = 123; b = a,相當于把 a 拷貝了一份給 b,但 a 是一個指針,所以此時 a 和 b 保存的地址是相同的,也就是指向了同一個對象。但 a+b 的時候則不是兩個指針相加,而是將a、b指向的對象進行相加,也就是操作變量會自動操作變量指向的內存。
因此在Python中,說傳遞方式是值傳遞或者引用傳遞都是不準確的,應該是變量的賦值傳遞,對象的引用傳遞。
原文鏈接:https://juejin.cn/post/7091845218299248647
相關推薦
- 2022-04-09 Maven 編譯提示:spring-boot-maven-plugin:2.1.9.RELEASE
- 2022-06-17 C#關鍵字之重寫override介紹_C#教程
- 2023-12-10 Failed to process, please exclude the tableName or
- 2022-10-31 ViewPager實現圖片切換效果_Android
- 2023-12-07 com.fasterxml.jackson.databind.ObjectMapper
- 2022-05-23 Android利用Palette實現提取圖片顏色_Android
- 2022-04-21 提升Python編碼能力的3個重要概念_python
- 2022-06-25 React服務端渲染和同構的實現_React
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支