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

时间:2009-07-20 08:07:04

标签: python iterator

作为一些WSGI中间件的一部分,我想编写一个包含迭代器的python类,以在迭代器上实现close方法。

当我尝试使用旧式类时,这可以正常工作,但是当我尝试使用新式类时会抛出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 个答案:

答案 0 :(得分:9)

你想要做的事情是有道理的,但这里有一些邪恶的东西在这里。

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()是一个迭代器,它可以动态修改它的下一个方法 - 在Python中的其他地方完全合法。我们创建的迭代器,它有我们期望的方法:it.next是next2。当我们直接使用迭代器时,通过调用next(),我们得到2.然而,当我们在for循环中使用它时,我们得到原始的下一个,我们已经明确地覆盖了它。

我不熟悉Python内部,但似乎对象的“下一个”方法正在tp_iternexthttp://docs.python.org/c-api/typeobj.html#tp_iternext)中缓存,然后在更改类时它不会更新。

这绝对是一个Python错误。也许这在生成器PEP中有描述,但它不在核心Python文档中,并且它与普通的Python行为完全不一致。

你可以通过保留原始的下一个函数并明确地包装它来解决这个问题:

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

......但这显然效率较低,你应该必须这样做。

答案 1 :(得分:6)

有很多地方CPython基于 class 属性而非实例属性采用令人惊讶的快捷方式。这是其中一个地方。

以下是一个演示此问题的简单示例:

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 foo很弱,所以我不得不在这里开始做一些有根据的猜测......但我相信它们是准确的。

当创建CPython类型时(也就是说,当解释器首先评估class块并且调用类'metaclass'__new__方法时),类型{{1}上的值} struct已初始化...因此,如果在创建PyTypeObject类型时,不存在DynamicNext方法,则next字段将设置为tp_iternext,从而导致{{1返回false。

现在,正如Glenn指出的那样,这几乎肯定是CPython中的一个错误...特别是考虑到正在测试的对象不可迭代或动态分配NULL方法时,纠正它只会影响性能( 非常大约):

PyIter_Check

编辑:经过一些挖掘后,修复程序就不那么简单了,因为至少代码的某些部分会假设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函数,而不是类型)。

SO!这就是为什么当您尝试使用动态分配的*it->ob_type->tp_iternext方法迭代新样式实例时会发生令人惊讶的事情。

答案 2 :(得分: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

所以解决方案是在otheriter

中返回__iter__
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实例。

答案 3 :(得分:3)

只需返回迭代器。这就是__iter__的用途。尝试将对象修补为迭代器并在已有迭代器时返回它是没有意义的。

编辑:现在有两种方法。有一次,猴子修补包裹的迭代器,第二,小猫包装迭代器。

class IteratorWrapperMonkey(object):

    def __init__(self, otheriter):
        self.otheriter = otheriter
        self.otheriter.close = self.close

    def close(self):
        print "Closed!"

    def __iter__(self):
        return self.otheriter

class IteratorWrapperKitten(object):

    def __init__(self, otheriter):
        self.otheriter = otheriter

    def __iter__(self):
        return self

    def next(self):
        return self.otheriter.next()

    def close(self):
        print "Closed!"

class PatchableIterator(object):

    def __init__(self, inp):
        self.iter = iter(inp)

    def next(self):
        return self.iter.next()

    def __iter__(self):
        return self

if __name__ == "__main__":
    monkey = IteratorWrapperMonkey(PatchableIterator([1, 2, 3]))
    for i in monkey:
        print i
    monkey.close()

    kitten = IteratorWrapperKitten(iter([1, 2, 3]))
    for i in kitten:
        print i
    kitten.close()

这两种方法都适用于新旧类。