《深度剖析CPython解释器》30. 源码解密内置函数 iter、next
楔子
这次我们来看看 iter 和 next 这两个内置函数的用法,我们知道 iter 是将一个可迭代对象变成一个迭代器,next 是将迭代器里的值一步一步迭代出来。
lst = [1, 2, 3]
it = iter(lst)
print(it) # <list_iterator object at 0x000001DC6E898640>
# 调用next, 可以对迭代器进行迭代
print(next(it)) # 1
注意: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(在底层被称为哨兵), 然后终止迭代
当然 next 函数也有一个特殊用法,就是它在接收一个迭代器的时候,还可以指定一个默认值;如果元素迭代完毕之后再次迭代的话,不会抛出StopIteration,而是会返回默认值。
it = iter([1, 2, 3])
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
print(next(it, "xxx")) # xxx
print(next(it, "yyy")) # yyy
注意:iter 内部接收可迭代对象的类型不同,那么得到的迭代器种类也不同。
print(iter("xyz")) # <str_iterator object at 0x00000234493B8640>
print(iter((1, 2, 3))) # <tuple_iterator object at 0x00000234493B8640>
print(iter([1, 2, 3])) # <list_iterator object at 0x00000234493B8640>
iter 函数底层实现
我们看一下 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);
}
所以核心就在于 PyObject_GetIter 中,它是根据可迭代对象生成迭代器的关键,那么它都做了哪些事情呢?不用想肯定是执行:obj.__iter__()
,当然更准确的说应该是:type(obj).__iter__(obj)
,我们来看一下。该函数定义在 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 里面的逻辑就是检测类型对象是否初始化了 tp_as_sequence->sq_item 成员, 它对应 __getitem__
if (PySequence_Check(o))
return PySeqIter_New(o);
// 走到这里说明该类型对象的实例对象不具备可迭代的性质, 抛出异常
return type_error("'%.200s' object is not iterable", o);
}
else {
// 否则的话, 直接进行调用, Py_TYPE(o)->tp_iter(o)
PyObject *res = (*f)(o);
// 如果返回值 res 不为NULL, 并且它还不是一个迭代器, 抛出异常
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__。
class A:
def __getitem__(self, item):
return f"参数item: {item}"
a = A()
# 内部定义了 __getitem__, 首先可以让实例对象像字典一样
print(a["name"]) # 参数item: name
print(a["夏色祭"]) # 参数item: 夏色祭
# 此外还可以像迭代器一个被for循环
for idx, val in enumerate(a):
print(val)
if idx == 5:
break
"""
参数item: 0
参数item: 1
参数item: 2
参数item: 3
参数item: 4
参数item: 5
"""
# 我们看到循环的时候会自动给item传值, 这个值是0 1 2 3...., 因此这个循环是无限的
class Girl:
def __init__(self):
self.names = ["夏色祭", "神乐七奈", "夜空梅露", "雫_るる"]
def __getitem__(self, item):
try:
val = self.names[item]
return val
except IndexError:
raise StopIteration # 让for循环结束
g = Girl()
for _ in g:
print(_)
"""
夏色祭
神乐七奈
夜空梅露
雫_るる
"""
# 当出现 StopIteration 异常的时候, 再次循环时传递的item会被重置为0
print(list(g)) # ['夏色祭', '神乐七奈', '夜空梅露', '雫_るる']
lst = []
lst.extend(g)
print(lst) # ['夏色祭', '神乐七奈', '夜空梅露', '雫_るる']
以上被称为解释器的退化功能,就是在找不到某个实现的时候,会进行退化、尝试寻找其它实现。类似的做法还有其它,比如:
class A:
def __len__(self):
return 0
# 当进行布尔判断的时候, 会尝试获取内部 __bool__ 方法的返回值
# 如果没有定义 __bool__, 那么解释器会退化尝试寻找 __len__ 方法
print(bool(A())) # False
如果内部定义了 __iter__,则直接调用即可。
# 如果type(obj)内部定义了__iter__, 那么iter(obj) <==> type(obj).__iter__(obj)
print(str.__iter__("123")) # <str_iterator object at 0x00000213CC2A8640>
print(list.__iter__([1, 2, 3])) # <list_iterator object at 0x00000213CC2A8640>
print(tuple.__iter__((1, 2, 3))) # <tuple_iterator object at 0x00000213CC2A8640>
print(set.__iter__({1, 2, 3})) # <set_iterator object at 0x00000213CC478E80>
next函数底层实现
了解 iter 之后,我们再来看看 next 函数;如果内部定义了 __next__ 函数,那么不用想,结果肯定是type(obj).__next__(obj)
,我们看一下底层实现。
static PyObject *
builtin_next(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *it, *res;
// 同样接收一个参数或者两个参数
if (!_PyArg_CheckPositional("next", nargs, 1, 2))
return NULL;
it = args[0];
// 第一个参数必须是一个迭代器, 也就是类型对象的tp_iternext成员不能为NULL
if (!PyIter_Check(it)) {
// 否则的话, 抛出TypeError, 表示第一个参数传递的不是一个迭代器
PyErr_Format(PyExc_TypeError,
"'%.200s' object is not an iterator",
it->ob_type->tp_name);
return NULL;
}
/*
列表对应的迭代器是: listiterobject, 其类型为: PyListIter_Type
元组对应的迭代器是: tupleiterobject, 其类型为: PyTupleIter_Type
字符串对应的迭代器是: unicodeiterobject, 其类型为: PyUnicodeIter_Type
*/
// 通过 ob_type 获取对应的类型对象, 然后获取成员 tp_iternext 的值, 相当于__next__
// 再传入迭代器进行迭代
res = (*it->ob_type->tp_iternext)(it);
// 如果 res 不为 NULL, 那么证明迭代到值了, 直接返回
if (res != NULL) {
return res;
} else if (nargs > 1) {
// 否则的话, 看nargs是否大于1, 如果大于1, 说明设置了默认值
PyObject *def = args[1];
// 出现异常的话, 将异常清空
if (PyErr_Occurred()) {
if(!PyErr_ExceptionMatches(PyExc_StopIteration))
return NULL;
PyErr_Clear();
}
// 增加引用计数, 并返回
Py_INCREF(def);
return def;
} else if (PyErr_Occurred()) {
return NULL;
} else {
PyErr_SetNone(PyExc_StopIteration);
return NULL;
}
}
还是比较简单的,我们以 列表 对应的迭代器为例,举个栗子:
lst = [1, 2, 3]
it = iter(lst)
# 对应的类型是list_iterator, 但在底层我们知道类型是PyListIter_Type
print(type(it)) # <class 'list_iterator'>
# 然后迭代
print(type(it).__next__(it)) # 1
print(type(it).__next__(it)) # 2
print(type(it).__next__(it)) # 3
# 以上就等价于 next(it), 但是我们知道内置函数 next 要更强大一些, 因为它还可以做一些其它处理
# 当然默认情况下, 和 type(it).__next__(it) 最终是殊途同归的
怎么样,是不是很简单呢?
我们看到一个变量 obj 不管指向什么可迭代对象,都可以交给 iter,得到对应的迭代器;不管什么迭代器,都可以交给 next 进行迭代。原因就在于它们接收的不是对象本身,而是对象对应的 PyObject * 泛型指针。不管你是谁的指针,只要你指向的对象是一个可迭代对象,那么都可以交给 iter。至于 next 也是同理,不管你指向的是哪一种迭代器,只要是迭代器,就可以交给 next,然后会自动调用迭代器内部的 __next__(底层是 tp_iternext)将值给迭代出来。所以这是不是相当于实现了多态呢?所以这就是 Python 的设计哲学,变量只是一个指针,传递变量的时候相当于传递指针(将指针拷贝一份),但是操作一个变量的时候会自动操作变量(指针)指向的内存。比如:a = 12345; b = a,相当于把 a 拷贝了一份给 b,但 a 是一个指针,所以此时 a 和 b 保存的地址是相同的,也就是指向了同一个对象。但 a + b 的时候则不是两个指针相加、而是将 a、b 指向的对象进行相加,也就是操作变量会操作变量指向的内存。因此在 Python 中,说传递方式是值传递或者引用传递都是不准确的,应该是变量之间的赋值传递,对象之间的引用传递。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏