【问题标题】:Iterable class in python3python3中的可迭代类
【发布时间】:2019-10-07 18:33:35
【问题描述】:

我正在尝试为 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>

永远。

怎么了?


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

【问题讨论】:

  • 请不要在世界其他地方使用self的地方使用me。为了与众不同而打破常规只会阻碍与他人的合作,包括在 Stack Overflow 上获得帮助。
  • __next__ 应该从迭代器中返回一个值,并且每次调用它时都应该返回一个不同的值。你让它产生每个值,这意味着它返回一个生成器。
  • 子类化list不是更容易吗?
  • 另一个旁注:有一个内置的iter() 函数,您的自定义iter() 函数会隐藏它,并且可能会导致那些希望找到原件的人感到困惑。 iter() 的实现可以替换为iter([1, 2, 3])(其中iter() 是内置函数)。
  • 您正在创建可迭代的实例而不是可迭代的。请参阅Iterating over object instances 了解 Python 3 中的可迭代类。

标签: python python-3.x iterator iterable


【解决方案1】:

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

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

因为你想创建一个 iterable,你可以在此处将 __iter__ 设为生成器:

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

请注意,生成器函数不应使用raise StopIteration,只需从函数返回即可。

上面的类是一个可迭代的。 Iterables 只有__iter__ 方法,没有__next__ 方法。当 __iter__ 被调用时,Iterables 会产生一个 iterator

Iterable -> (调用__iter__) -> 迭代器

在上面的例子中,因为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__()(迭代器)返回的对象上调用了__next__(),而不是在test 本身上调用了__next__(),它没有那个方法,因为它只是一个可迭代的,而不是一个迭代器。

迭代器还有一个__iter__ 方法,它总是必须返回self,因为它们是它们自己的迭代器。正是__next__ 方法使它们成为迭代器,__next__ 的工作是重复调用,直到引发StopIteration。在引发StopIteration 之前,每次调用都应返回下一个值。一旦迭代器完成(已引发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__ 调用。这有点作弊,将迭代器的状态隐藏在现成的标准库对象中。

您通常不会在 Python 代码中看到任何调用 __iter____next__ 的内容,因为这两个方法只是您可以在 Python 类中实现的钩子;如果您要在 C API 中实现迭代器,那么钩子名称会略有不同。相反,您可以使用 iter()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_NAMEt 设置为下一个值,然后跳转回位置 4),直到出现StopIteration。一旦发生这种情况,它将跳转到位置 10 以结束循环。

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

>>> 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 返回一个自定义迭代器类实例(而不是让生成器函数为我们创建迭代器):

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 类,但TestIterator 保留了对Test 实例的引用。 tuple_iterator 也是这样工作的。

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


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

【讨论】:

    【解决方案2】:

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

    您需要明确您希望Test 是可迭代的还是迭代器。如果它是可迭代的,它将能够提供带有__iter__ 的迭代器。如果它是一个迭代器,它将能够为新元素提供__next__。迭代器通常可以通过在__iter__ 中返回自身来作为可迭代对象。 Martijn's answer 显示您可能想要的内容。但是,如果您想要一个如何具体实现 __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)
    

    【讨论】:

      【解决方案3】:

      我不清楚您到底想要实现什么,但如果您真的想使用这样的实例属性,您可以将输入转换为生成器,然后对其进行迭代。但是,正如我所说,这感觉很奇怪,我认为您实际上并不想要这样的设置。

      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)
      

      【讨论】:

      • 我特别放弃了在实例中存储迭代器; (i for i in ids) 在此处通过生成器表达式创建迭代器。你可以改用self.ids = iter(ids)
      • 这仍然将迭代委托给存储的迭代器。没关系,但只需使用test = iter([1, 2, 3]) 并省去没有添加任何内容的薄包装。 :-)
      • 这正是我对 OP 的问题感到困惑的地方,因为我不明白他们的目标是什么或他们想如何使用它。我将按原样保留答案,并注意这仅用于说明目的。它按照要求做,但实际上完全没用。
      【解决方案4】:

      最简单的解决方案是使用__iter__并将迭代器返回到主列表:

      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)
      

      作为更新,对于延迟加载,您可以将迭代器返回给生成器:

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

      【讨论】:

      • 当然,但这充其量只是一个旁注。这实际上并没有告诉 OP 关于他们的方法失败的原因。
      • 此解决方案对我不起作用,但指向我更新问题。图像(由 id 表示)是延迟获取的。
      • @Vorac,所以只需将迭代器返回给加载文件的生成器
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-03-22
      • 2014-05-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-20
      相关资源
      最近更新 更多