python3 中的可迭代 class

Iterable class in python3

我正在尝试为 Web 资源(延迟获取的图像)实现可迭代代理。

首先,我做了(返回 id,在生产中这些将是图像缓冲区)

def iter(ids=[1,2,3]):
    for id in ids:
        yield id

效果很好,但现在我需要保持状态。

我读了the four ways to define iterators。我判断迭代器协议是要走的路。按照我的尝试和失败来实现它。

class Test:
    def __init__(self, ids):
         self.ids = ids
    def __iter__(self):
        return self
    def __next__(self):
        for id in self.ids:
            yield id
        raise StopIteration

test = Test([1,2,3])
for t in test:
    print('new value', t)

输出:

new value <generator object Test.__next__ at 0x7f9c46ed1750>
new value <generator object Test.__next__ at 0x7f9c46ed1660>
new value <generator object Test.__next__ at 0x7f9c46ed1750>
new value <generator object Test.__next__ at 0x7f9c46ed1660>
new value <generator object Test.__next__ at 0x7f9c46ed1750>

永远。

怎么了?


非常感谢大家!这对我来说都是新的,但我正在学习新的很酷的东西。

您的 __next__ 方法使用 yield,这使它成为一个 生成器函数 。生成器函数 return 调用时生成一个新的迭代器。

但是 __next__ 方法是 迭代器接口的一部分 。它本身不应该是一个迭代器。 __next__ 应该 return 下一个值,而不是 return 所有值 (*).

因为你想创建一个iterable,你可以在这里制作__iter__生成器:

class Test:
    def __init__(self, ids):
         self.ids = ids
    def __iter__(self):
        for id in self.ids:
            yield id

请注意,生成器函数 不应使用 raise StopIteration,只需 return 从函数中为您执行此操作即可。

上面的class是一个可迭代对象。 Iterables 只有 __iter__ 方法, 而没有 __next__ 方法 。 Iterables 在调用 __iter__ 时产生一个 iterator

Iterable -> (call __iter__) -> Iterator

在上面的例子中,因为Test.__iter__是一个生成器函数,我们每次调用它都会创建一个新的对象:

>>> test = Test([1,2,3])
>>> test.__iter__()  # create an iterator
<generator object Test.__iter__ at 0x111e85660>
>>> test.__iter__()
<generator object Test.__iter__ at 0x111e85740>

生成器对象 是一种特定的迭代器,它是通过调用生成器函数或使用生成器表达式创建的。请注意,表示中的十六进制值不同,为两次调用创建了两个不同的对象。这是设计使然! Iterables 产生迭代器,并且可以随意创建更多。这让你可以独立地遍历它们:

>>> test_it1 = test.__iter__()
>>> test_it1.__next__()
1
>>> test_it2 = test.__iter__()
>>> test_it2.__next__()
1
>>> test_it1.__next__()
2

请注意,我在迭代器 test.__iter__() 编辑的对象 return 上调用了 __next__(),而不是在 test 本身上调用了 __next__(),后者没有该方法,因为它只是一个可迭代对象,不是迭代器。

迭代器也有一个__iter__方法,它总是必须returnself,因为它们是它们自己的迭代器。正是 __next__ 方法使它们成为迭代器,__next__ 的工作将被重复调用,直到引发 StopIteration。在引发 StopIteration 之前,每次调用都应该 return 下一个值。一旦迭代器完成(引发了 StopIteration),它就意味着总是引发 StopIteration。迭代器只能使用一次,除非它们是无限的(永远不要引发 StopIteration 并且每次调用 __next__ 时都保持产生值)。

所以这是一个迭代器:

class IteratorTest:
    def __init__(self, ids):
        self.ids = ids
        self.nextpos = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.ids is None or self.nextpos >= len(self.ids):
            # we are done
            self.ids = None
            raise StopIteration
        value = self.ids[self.nextpos]
        self.nextpos += 1
        return value

这需要做更多的工作;它必须跟踪下一个要产生的值是什么,以及我们是否已经筹集到 StopIteration。这里的其他回答者使用了看似更简单的方法,但实际上涉及让 else 完成所有艰苦的工作。当您使用 iter(self.ids)(i for i in ids) 时,您正在创建一个不同的迭代器来委托 __next__ 调用。这有点作弊,将迭代器的状态隐藏在 ready-made 标准库对象中。

您通常不会在 Python 代码中看到任何调用 __iter____next__ 的东西,因为这两个方法只是您可以在 Python classes;如果您要在 C API 中实现迭代器,则挂钩名称会略有不同。相反,您要么使用 iter() and next() 函数,要么只使用语法中的对象或接受可迭代对象的函数调用。

for循环就是这样的语法。当您使用 for 循环时,Python 使用(道德等价物)调用对象上的 __iter__(),然后对生成的迭代器对象调用 __next__() 以获取每个值。如果你 disassemble the Python bytecode:

你可以看到这个
>>> from dis import dis
>>> dis("for t in test: pass")
  1           0 LOAD_NAME                0 (test)
              2 GET_ITER
        >>    4 FOR_ITER                 4 (to 10)
              6 STORE_NAME               1 (t)
              8 JUMP_ABSOLUTE            4
        >>   10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

位置2的GET_ITER操作码调用test.__iter__(),而FOR_ITER在生成的迭代器上使用__next__来保持循环(执行STORE_NAME设置t 到下一个值,然后跳回到位置 4),直到引发 StopIteration。一旦发生这种情况,它将跳转到位置 10 以结束循环。

如果您想更多地了解迭代器和可迭代对象之间的区别,请查看 Python 标准类型,看看当您使用 iter()next() 时会发生什么他们。喜欢列表或元组:

>>> foo = (42, 81, 17, 111)
>>> next(foo)  # foo is a tuple, not an iterator
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object is not an iterator
>>> t_it = iter(foo)  # so use iter() to create one from the tuple
>>> t_it   # here is an iterator object for our foo tuple
<tuple_iterator object at 0x111e9af70>
>>> iter(t_it)  # it returns itself
<tuple_iterator object at 0x111e9af70>
>>> iter(t_it) is t_it  # really, it returns itself, not a new object
True
>>> next(t_it)  # we can get values from it, one by one
42
>>> next(t_it)  # another one
81
>>> next(t_it)  # yet another one
17
>>> next(t_it)  # this is getting boring..
111
>>> next(t_it)  # and now we are done
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> next(t_it)  # an *stay* done
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> foo  # but foo itself is still there
(42, 81, 17, 111)

您也可以使 Test、可迭代对象 return 成为自定义迭代器 class 实例(而不是通过让生成器函数为我们创建迭代器来应对):

class Test:
    def __init__(self, ids):
        self.ids = ids
    def __iter__(self):
        return TestIterator(self)

class TestIterator:
    def __init__(self, test):
        self.test = test
    def __iter__(self):
        return self
def __next__(self):
    if self.test is None or self.nextpos >= len(self.test.ids):
        # we are done
        self.test = None
        raise StopIteration
    value = self.test.ids[self.nextpos]
    self.nextpos += 1
    return value

这很像上面的原始 IteratorTest class,但是 TestIterator 保留对 Test 实例的引用。 tuple_iterator 也是如此。

这里是关于命名约定的最后一个简短说明:我坚持使用 self 作为方法的第一个参数,即绑定实例。为该参数使用不同的名称只会使与其他有经验的 Python 开发人员谈论您的代码变得更加困难。不要使用 me,无论它看起来多么可爱或简短。


(*) 除非你的目标是创建迭代器的迭代器,当然(这基本上是 itertools.groupby() iterator 所做的,它是一个迭代器产生 (object, group_iterator) 元组,但我离题了)。

__next__ 函数应该 return 迭代器提供的下一个值。由于您在实现中使用了 yield,因此函数 return 是一个生成器,这就是您所得到的。

你需要明确你希望 Test 是可迭代对象还是迭代器。如果它是一个可迭代对象,它将能够提供一个带有 __iter__ 的迭代器。如果它是一个迭代器,它将有能力提供新元素 __next__。迭代器通常可以通过 return 在 __iter__ 中自身作为可迭代对象工作。 显示了您可能想要的内容。但是,如果您想要一个示例来说明如何具体实现 __next__(通过使 Test 明确成为迭代器),它可能是这样的:

class Test:
    def __init__(self, ids):
        self.ids = ids
        self.idx = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.idx >= len(self.ids):
            raise StopIteration
        else:
            self.idx += 1
            return self.ids[self.idx - 1]

test = Test([1,2,3])
for t in test:
    print('new value', t)

我不清楚你到底想达到什么目的,但如果你真的想像这样使用你的实例属性,你可以将输入转换为生成器,然后这样迭代它。但是,正如我所说,这感觉很奇怪,我不认为你真的想要这样的设置。

class Test:
    def __init__(self, ids):
         self.ids = iter(ids)
    def __iter__(self):
        return self
    def __next__(self):
        return next(self.ids)

test = Test([1,2,3])
for t in test:
    print('new value', t)

最简单的解决方案是使用 __iter__ 和 return 主列表的迭代器:

class Test:
    def __init__(self, ids):
         self.ids = ids
    def __iter__(self):
        return iter(self.ids)

test = Test([1,2,3])
for t in test:
    print('new value', t)

作为更新,对于延迟加载,您可以 return 生成器的迭代器:

    def __iter__(self):
        return iter(load_file(id) for id in self.ids)