如何构建一个基本的迭代器?
如何构建一个基本的迭代器?
我如何在Python中创建一个迭代器?\n例如,假设我有一个类,其实例在逻辑上“包含”一些值:\n
class Example: def __init__(self, values): self.values = values
\n我希望能够编写如下代码:\n
e = Example([1, 2, 3]) # 每次循环时,从e.values中暴露一个值 for value in e: print("The example object contains", value)
\n更一般地说,迭代器应该能够控制值的来源,甚至可以即时计算值(而不考虑实例的任何特定属性)。
如何构建一个基本的迭代器?
在讨论中,某些情况下在__iter__中使用return self的情况。他指出,__iter__本身可以是一个生成器,这样就不需要__next__和引发StopIteration异常。
这是一个例子:
class range: def __init__(self, a, b): self.a = a self.b = b def __iter__(self): i = self.a while i < self.b: yield i i += 1
当然,对于更复杂的类来说,直接创建一个生成器可能更方便。
有人评论说,只使用return self在__iter__中有些无聊。当他尝试在其中使用yield时,他发现你的代码正好实现了他想要尝试的功能。
但在这种情况下,如何实现next()呢?是return iter(self).next()吗?
实际上,它已经“实现”了,因为iter(self)返回的是一个迭代器,而不是range实例。
使用iter(range(5,10)).next()有些繁琐。诚然,这是next行为的一个糟糕例子。我仍然对如何给range实例添加next属性感兴趣。
这是最简单的方法,不需要跟踪self.current或任何其他计数器。这应该是最受欢迎的答案!
区别在于,__iter__作为生成器是一个不同的对象,而不是range()实例。有时这很重要,有时不重要。
你不应该使用iter(range(5,10)).next()。正确的方式是next(iter(range(5,10)))。next内置函数恰好可以让你不必关心self在这种情况下是返回还是不返回。
这种方法对于类似于r = range(5); list_of_lists = list([ri, list(r)] for ri in r)的情况更像是预期的。
有趣的是,__iter__并不一定要引发StopIteration异常。只定义__iter__的一个问题是,如果__next__不返回单个项,那么next(myiterator)是无法工作的。使用next(iter(myiterator))并不是一个明智的替代方法。
需要明确的是,这种方法使得你的类是可迭代的,但不是迭代器。每次在类的实例上调用iter时,都会得到新的迭代器,但它们本身不是类的实例。
在Python 2中,iter(range(5,10)).next()和next(iter(range(5,10)))已经完全等价。next作为一个函数的优势与__iter__是否返回self无关(对于这两个代码片段的行为是相同的)。next内置函数的优势是:1. 在Py2和Py3上都可以使用,即使方法在它们之间更改名称;2. 当适用时,它可以给定第二个参数,在迭代器已经耗尽的情况下返回,而不是引发StopIteration异常。
如何构建一个基本迭代器?
构建迭代器的四种方法有:
1. 创建一个生成器(使用yield关键字)
2. 使用生成器表达式(genexp)
3. 创建一个迭代器(定义__iter__和__next__方法)
4. 创建一个Python可以自动迭代的类(定义__getitem__方法)
以下是四种方法的示例代码:
# 生成器 def uc_gen(text): for char in text.upper(): yield char # 生成器表达式 def uc_genexp(text): return (char for char in text.upper()) # 迭代器协议 class uc_iter(): def __init__(self, text): self.text = text.upper() self.index = 0 def __iter__(self): return self def __next__(self): try: result = self.text[self.index] except IndexError: raise StopIteration self.index += 1 return result # getitem方法 class uc_getitem(): def __init__(self, text): self.text = text.upper() def __getitem__(self, index): return self.text[index]
要看到这四种方法的效果,可以按照以下方式进行迭代:
for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem: for ch in iterator('abcde'): print(ch, end=' ') print()
上述代码的输出结果为:
A B C D E A B C D E A B C D E A B C D E
需要注意的是,两种生成器类型(uc_gen和uc_genexp)不能使用reversed()函数进行反向迭代;纯迭代器(uc_iter)需要实现__reversed__魔法方法,但是返回self也可以正常工作;而getitem方法(uc_getitem)需要实现__len__魔法方法。
在回答Colonel Panic的关于无限惰性求值迭代器的问题时,可以使用以上四种方法的示例代码:
# 生成器 def even_gen(): result = 0 while True: yield result result += 2 # 生成器表达式 def even_genexp(): return (num for num in even_gen()) # 迭代器协议 class even_iter(): def __init__(self): self.value = 0 def __iter__(self): return self def __next__(self): next_value = self.value self.value += 2 return next_value # getitem方法 class even_getitem(): def __getitem__(self, index): return index * 2 import random for iterator in even_gen, even_genexp, even_iter, even_getitem: limit = random.randint(15, 30) count = 0 for even in iterator(): print(even, end=' ') count += 1 if count >= limit: break print()
上述代码的输出结果为(至少在我的样本运行中):
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32
如何选择使用哪种方法?这主要是个人口味的问题。我经常看到使用生成器和迭代器协议的方法,还有一种混合方式(__iter__返回一个生成器)。
生成器表达式对于替代列表生成式很有用(它们是惰性的,因此可以节省资源)。
如果需要与早期的Python 2.x版本兼容,可以使用getitem方法。
我喜欢这个总结,因为它是完整的。这三种方法(yield、生成器表达式和迭代器)本质上是相同的,尽管有些更方便。yield操作符捕获了“续行”,其中包含状态(例如我们正在进行的索引)。信息保存在“续行”的闭包中。迭代器方式将相同的信息保存在迭代器的字段中,这本质上是一个闭包。getitem方法有点不同,因为它索引到内容中,而不是迭代性的。
在uc_getitem()中,你没有增加索引。实际上,经过深思熟虑,它不应该增加索引,因为它没有维护索引。但它也不是一种抽象迭代的方式。
实际上,它是的。在以上四种情况下,可以使用相同的代码进行迭代。
我不是专家,但是我们不应该在uc_iter类中重置索引吗?例如,在iter方法中将self.index设置为0,以便下一次迭代器的调用正常工作。
不,uc_iter的实例应该在完成后过期(否则它将无限循环);如果想再次进行迭代,必须再次调用uc_iter()来获取新的迭代器。
uc_getitem()可以工作,但可能被视为向后兼容的选项。它在引发IndexError时停止。
您可以在iter方法中设置self.index = 0,以便可以多次迭代。否则无法。
如果您可以抽出时间,我将解释为什么会选择其中一种方法而不是其他方法。
绝对不行,因为这将违反迭代器协议。迭代器期望在__iter__()中只返回自身,以便iter(iterator_instance)不会改变给定迭代器实例的状态。
如果要多次迭代复杂的MyClass对象,请创建一个只有__init__和__next__方法的MyClassIterator类,并从MyClass.__iter__中返回该类的实例(例如return MyClassIterator(self)),而不是返回self,这样MyClassIterator可以存储对MyClass实例的引用以及当前的数据索引,从而在满足其他迭代器协议问题的情况下可以同时多次调用MyClassIterator。
如何选择使用哪种方法?这主要是个人口味的问题。我经常看到使用生成器和迭代器协议的方法,还有一种混合方式(__iter__返回一个生成器)。
生成器表达式对于替代列表生成式很有用(它们是惰性的,因此可以节省资源)。
如果需要与早期的Python 2.x版本兼容,可以使用getitem方法。
以上是关于如何构建基本迭代器的内容。这四种方法各有优劣,选择哪种方法主要取决于个人偏好和具体需求。
如何构建一个基本的迭代器?
迭代器对象在Python中遵循迭代器协议,这基本上意味着它们提供两个方法:__iter__()和__next__()。
- __iter__()方法返回迭代器对象,会在循环开始时隐式调用。
- __next__()方法返回下一个值,并在每次循环递增时隐式调用。当没有更多的值返回时,该方法会引发StopIteration异常,这会被循环结构隐式捕获以停止迭代。
这里有一个简单的计数器示例:
class Counter: def __init__(self, low, high): self.current = low - 1 self.high = high def __iter__(self): return self def __next__(self): self.current += 1 if self.current < self.high: return self.current raise StopIteration for c in Counter(3, 9): print(c)
这将打印:
3 4 5 6 7 8
使用生成器更容易编写,如前面的答案中所述:
def counter(low, high): current = low while current < high: yield current current += 1 for c in counter(3, 9): print(c)
输出的结果将是相同的。在底层,生成器对象支持迭代器协议,并且与Counter类的工作原理大致相似。
David Mertz的文章《迭代器和简单生成器》是一个很好的入门介绍。
这基本上是一个很好的答案,但它返回self有点不太优化。例如,如果在嵌套的for循环中使用相同的计数器对象,可能得不到预期的行为。
不,迭代器应该返回自身。可迭代对象返回迭代器,但可迭代对象不应该实现__next__()。counter是一个迭代器,但它不是一个序列。它不存储它的值。例如,你不应该在嵌套的for循环中使用计数器。
在Counter示例中,self.current应该在__iter__()中赋值(除了在__init__()中)。否则,该对象只能被迭代一次。例如,如果你说ctr = Counters(3, 8),那么你不能多次使用for c in ctr。
__iter__()方法中的代码是否应该设置self.current的值?
绝对不应该。Counter是一个迭代器,迭代器只应该被迭代一次。如果在__iter__()中重置self.current,那么对Counter的嵌套循环将完全失效,并且迭代器的所有假设行为(调用iter对它们是幂等的)都被违反。如果你想要能够多次迭代ctr,它需要是一个非迭代器可迭代对象,在每次调用__iter__()时返回一个全新的迭代器。试图混合和匹配(在调用__iter__()时隐式重置迭代器)会违反协议。
例如,如果Counter要成为一个非迭代器可迭代对象,你将完全删除__next__/next的定义,并且可能会重新定义__iter__为与本答案末尾描述的生成器函数相同的形式(除了边界从__iter__的参数到__init__的参数,保存在self上,并在__iter__中从self访问)。
顺便说一句,如果你想编写可移植的迭代器类,一个有用的做法是定义next或__next__中的一个,然后将一个名称分配给另一个(next = __next__或__next__ = next,具体取决于你给方法的名称)。这样定义两个名称意味着它可以在Py2和Py3上都工作,无需更改源代码。
感谢回答。为了澄清一种模棱两可的情况:__iter__()在进入循环结构之前被调用一次。"......在循环的开始处"表明__iter__()在同一循环结构的每次循环开始之前被调用,这是错误的。使用Counter的双重嵌套for循环将会显示每次循环之前__iter__()只被调用一次,并且在嵌套的for循环执行之前。