Python迭代器 – 如何在新式类中动态分配self.next?

12 投票
4 回答
8645 浏览
提问于 2025-04-15 13:01

在一些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 个回答

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

6

在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方法的新式实例时,会发生一些意想不到的事情。

9

你想做的事情是有道理的,但在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

...但这显然效率更低,而且你不应该这样做。

撰写回答