Python迭代器 – 如何在新式类中动态分配self.next?
在一些WSGI中间件的工作中,我想写一个Python类,这个类可以包装一个迭代器,并在这个迭代器上实现一个关闭方法。
当我用旧式类来做这个时,一切都很顺利,但当我用新式类时却出现了类型错误(TypeError)。我需要做些什么才能让新式类正常工作呢?
举个例子:
class IteratorWrapper1:
def __init__(self, otheriter):
self._iterator = otheriter
self.next = otheriter.next
def __iter__(self):
return self
def close(self):
if getattr(self._iterator, 'close', None) is not None:
self._iterator.close()
# other arbitrary resource cleanup code here
class IteratorWrapper2(object):
def __init__(self, otheriter):
self._iterator = otheriter
self.next = otheriter.next
def __iter__(self):
return self
def close(self):
if getattr(self._iterator, 'close', None) is not None:
self._iterator.close()
# other arbitrary resource cleanup code here
if __name__ == "__main__":
for i in IteratorWrapper1(iter([1, 2, 3])):
print i
for j in IteratorWrapper2(iter([1, 2, 3])):
print j
输出结果如下:
1
2
3
Traceback (most recent call last):
...
TypeError: iter() returned non-iterator of type 'IteratorWrapper2'
4 个回答
看起来内置的 iter
并不是检查实例中的 next
可调用方法,而是检查类本身。而 IteratorWrapper2
里没有任何 next
方法。下面是你问题的一个简单版本。
class IteratorWrapper2(object):
def __init__(self, otheriter):
self.next = otheriter.next
def __iter__(self):
return self
it=iter([1, 2, 3])
myit = IteratorWrapper2(it)
IteratorWrapper2.next # fails that is why iter(myit) fails
iter(myit) # fails
所以解决办法是在 __iter__
方法中返回 otheriter
。
class IteratorWrapper2(object):
def __init__(self, otheriter):
self.otheriter = otheriter
def __iter__(self):
return self.otheriter
或者你可以自己写一个 next
方法,来包装内部的迭代器。
class IteratorWrapper2(object):
def __init__(self, otheriter):
self.otheriter = otheriter
def next(self):
return self.otheriter.next()
def __iter__(self):
return self
不过我不明白为什么 iter
不直接使用实例的 self.next
。
在CPython中,有很多地方会根据类的特性而不是实例的特性来采取一些意想不到的捷径。这就是其中一个例子。
下面是一个简单的例子,能展示这个问题:
def DynamicNext(object):
def __init__(self):
self.next = lambda: 42
接下来,我们看看会发生什么:
>>> instance = DynamicNext() >>> next(instance) … TypeError: DynamicNext object is not an iterator >>>
现在,深入研究CPython的源代码(版本2.7.2),我们可以看到next()
这个内置函数的实现:
static PyObject *
builtin_next(PyObject *self, PyObject *args)
{
…
if (!PyIter_Check(it)) {
PyErr_Format(PyExc_TypeError,
"%.200s object is not an iterator",
it->ob_type->tp_name);
return NULL;
}
…
}
还有PyIter_Check
的实现:
#define PyIter_Check(obj) \
(PyType_HasFeature((obj)->ob_type, Py_TPFLAGS_HAVE_ITER) && \
(obj)->ob_type->tp_iternext != NULL && \
(obj)->ob_type->tp_iternext != &_PyObject_NextNotImplemented)
第一行PyType_HasFeature(…)
,在展开所有常量和宏之后,相当于DynamicNext.__class__.__flags__ & 1L<<17 != 0
:
>>> instance.__class__.__flags__ & 1L<<17 != 0 True
所以这个检查显然没有失败……这就意味着下一个检查——(obj)->ob_type->tp_iternext != NULL
——是失败的。
在Python中,这一行大致相当于hasattr(type(instance), "next")
:
>>> type(instance) __main__.DynamicNext >>> hasattr(type(instance), "next") False
显然失败了,因为DynamicNext
类型并没有next
方法——只有该类型的实例才有。
现在,我对CPython的理解不深,所以我得开始做一些有根据的猜测……但我相信这些猜测是准确的。
当创建一个CPython类型时(也就是说,当解释器第一次评估class
块并调用类的元类的__new__
方法时),类型的PyTypeObject
结构中的值会被初始化……所以如果在创建DynamicNext
类型时,没有next
方法存在,tp_iternext
字段就会被设置为NULL
,这会导致PyIter_Check
返回false。
正如Glenn所指出的,这几乎肯定是CPython中的一个bug……尤其是考虑到修复它只会在被测试的对象不可迭代或动态分配了next
方法时影响性能(大致来说):
#define PyIter_Check(obj) \
(((PyType_HasFeature((obj)->ob_type, Py_TPFLAGS_HAVE_ITER) && \
(obj)->ob_type->tp_iternext != NULL && \
(obj)->ob_type->tp_iternext != &_PyObject_NextNotImplemented)) || \
(PyObject_HasAttrString((obj), "next") && \
PyCallable_Check(PyObject_GetAttrString((obj), "next"))))
编辑:经过一些深入研究,修复并不会这么简单,因为代码的某些部分假设,如果PyIter_Check(it)
返回true
,那么*it->ob_type->tp_iternext
就会存在……但这并不一定成立(也就是说,因为next
函数存在于实例上,而不是类型上)。
所以!这就是为什么当你尝试迭代一个具有动态分配next
方法的新式实例时,会发生一些意想不到的事情。
你想做的事情是有道理的,但在Python内部发生了一些奇怪的事情。
class foo(object):
c = 0
def __init__(self):
self.next = self.next2
def __iter__(self):
return self
def next(self):
if self.c == 5: raise StopIteration
self.c += 1
return 1
def next2(self):
if self.c == 5: raise StopIteration
self.c += 1
return 2
it = iter(foo())
# Outputs: <bound method foo.next2 of <__main__.foo object at 0xb7d5030c>>
print it.next
# 2
print it.next()
# 1?!
for x in it:
print x
foo()是一个迭代器,它的next方法可以动态修改——在Python的其他地方这是完全合法的。我们创建的这个迭代器it,拥有我们期待的方法:it.next是next2。当我们直接使用这个迭代器,调用next()时,我们得到了2。然而,当我们在for循环中使用它时,却得到了原来的next,明明我们已经把它覆盖了。
我对Python的内部机制不太了解,但看起来一个对象的“next”方法被缓存到了tp_iternext
中(http://docs.python.org/c-api/typeobj.html#tp_iternext),然后在类被修改时并没有更新。
这绝对是Python的一个bug。也许在生成器的PEP文档中有描述,但在Python的核心文档里没有,而且这和正常的Python行为完全不一致。
你可以通过保留原来的next函数,并显式地将其包装起来来解决这个问题:
class IteratorWrapper2(object):
def __init__(self, otheriter):
self.wrapped_iter_next = otheriter.next
def __iter__(self):
return self
def next(self):
return self.wrapped_iter_next()
for j in IteratorWrapper2(iter([1, 2, 3])):
print j
...但这显然效率更低,而且你不应该这样做。