如何构建一个基本的迭代器?

691
如何在Python中创建一个迭代器
例如,假设我有一个类,其实例在逻辑上“包含”一些值:
class Example:
    def __init__(self, values):
        self.values = values

我想要能够写出像这样的代码:
e = Example([1, 2, 3])
# Each time through the loop, expose one of the values from e.values
for value in e:
    print("The example object contains", value)

更一般地说,迭代器应该能够控制值的来源,甚至可以在需要时动态计算值(而不仅仅考虑实例的特定属性)。
10个回答

759

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): # Python 2: 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)

打印输出将保持不变。在幕后,生成器对象支持迭代器协议,并且执行类计数器的大致相似操作。

David Mertz的文章,迭代器和简单生成器,是一个很好的介绍。


8
这基本上是一个很好的答案,但事实上它返回自身有点不够优化。例如,如果您在双重嵌套的for循环中使用相同的计数器对象,可能得不到您想要的行为。 - Casey Rodarmor
37
不,迭代器应该返回它们自己。可迭代对象返回迭代器,但可迭代对象不应该实现 __next__ 方法。counter 是一个迭代器,但它不是一个序列。它不会存储它的值。例如,你不应该在双重嵌套的 for 循环中使用计数器。 - leewz
5
在这个反例中,应该在__iter__中对self.current进行赋值(除了在__init__中)。否则,对象只能被迭代一次。例如,如果你写了ctr = Counters(3, 8),那么你不能使用for c in ctr超过一次。 - Curt
10
@Curt: 绝对不行。Counter 是一个迭代器,而迭代器只应该被迭代一次。如果你在 __iter__ 中重置了 self.current,那么对 Counter 的嵌套循环将会完全破坏,并且所有迭代器的预设行为(对它们调用 iter 是幂等的)都将被违反。如果你想要能够多次迭代 ctr,它就需要是一个非迭代器可迭代对象,在每次调用 __iter__ 时返回一个全新的迭代器。试图混合和匹配(一个在调用 __iter__ 时隐式重置的迭代器)会违反协议。 - ShadowRanger
4
例如,如果Counter将成为一个非迭代器的可迭代对象,您需要完全删除__next__/next的定义,并且可能会重新定义__iter__为与本答案末尾描述的生成器相同形式的生成器函数(除了边界不是来自于 __iter__参数,而是来自于存储在self上并在__iter__中从self访问的__init__参数)。 - ShadowRanger
显示剩余3条评论

534

有四种构建迭代函数的方法:

例子:

# generator
def uc_gen(text):
    for char in text.upper():
        yield char

# generator expression
def uc_genexp(text):
    return (char for char in text.upper())

# iterator protocol
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 method
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_genuc_genexp)不能使用reversed(); 普通迭代器(uc_iter)需要实现__reversed__魔法方法(根据文档, 必须返回一个新的迭代器, 但在CPython中返回self也可以工作); 而getitem可迭代对象(uc_getitem)必须实现__len__魔法方法:

    # for uc_iter we add __reversed__ and update __next__
    def __reversed__(self):
        self.index = -1
        return self
    def __next__(self):
        try:
            result = self.text[self.index]
        except IndexError:
            raise StopIteration
        self.index += -1 if self.index < 0 else +1
        return result

    # for uc_getitem
    def __len__(self)
        return len(self.text)
为了回答Colonel Panic的有关无限延迟评估迭代器的附加问题,以下是使用上述四种方法的示例:
# generator
def even_gen():
    result = 0
    while True:
        yield result
        result += 2


# generator expression
def even_genexp():
    return (num for num in even_gen())  # or even_iter or even_getitem
                                        # not much value under these circumstances

# iterator protocol
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 method
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,
        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__

4
我喜欢这个总结,因为它很完整。其中那三种方法(yield、生成器表达式和迭代器)本质上是相同的,尽管有些比其他方法更方便。yield 运算符捕获了“继续执行”的状态(例如我们现在的索引),这些信息保存在“闭包”中。迭代器的方式将相同的信息保存在迭代器的字段中,本质上与闭包相同。getitem 方法略有不同,因为它对内容进行索引,而不是具有迭代性质。 - Ian
2
@metaperl:实际上是这样的。在上述所有四种情况中,您都可以使用相同的代码进行迭代。 - Ethan Furman
3
你可以在__iter__中设置self.index = 0,这样你就可以多次迭代。否则就不行。 - John Strood
1
如果您有时间,我会很感激您能解释一下为什么您会选择其中任何一种方法。 - aaaaaa
1
@t3chb0t:这四个even_*名称形成一个元组,因此for循环将运行四次,iterator被分配给even_gen,然后是even_genexp,然后是even_iter,最后是even_getitem。这不是一个包含检查(即'a' in 'abc')。 - undefined
显示剩余14条评论

130

我看到有些人在__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

当然,这里人们可能直接创建一个生成器,但是对于更复杂的类,这种方法可能会很有用。


5
太好了!在__iter__中只写return self太无聊了。当我想尝试在其中使用yield时,我发现你的代码恰好就是我想尝试的。 - Ray
3
在这种情况下,如何实现next()方法呢?是使用return iter(self).next()吗? - Lenna
4
@Lenna,它已经“实现”了,因为iter(self)返回一个迭代器,而不是range实例。 - Manux
3
这是最简单的方法,不需要跟踪例如“self.current”或任何其他计数器。这应该是票数最高的答案! - astrofrog
15
要明确的是,这种方法使您的类成为可迭代,但不是迭代器。每次在类的实例上调用iter时,您都会获得新的迭代器,但它们本身不是类的实例。 - ShadowRanger
显示剩余6条评论

105

首先,itertools 模块 对于所有需要迭代器的情况都非常有用,但是这里只需要在 Python 中创建一个迭代器:

yield

很酷吧?在函数中,Yield 可以用来替换普通的return。它返回对象,但是不会销毁状态并退出,而是保存状态以便下一次迭代执行。下面是一个直接从 itertools 函数列表 提取的示例:

def count(n=0):
    while True:
        yield n
        n += 1

如函数描述中所述(它是itertools模块中的count()函数...),它生成一个迭代器,返回从n开始的连续整数。

生成器表达式是另一种完全不同的东西(很棒的东西!)。它们可以用来取代列表推导式以节省内存(列表推导式在内存中创建一个列表,如果没有分配给变量,则在使用后被销毁,但生成器表达式可以创建一个生成器对象...这是说Iterator的一种花哨方式)。以下是生成器表达式定义的示例:

gen = (n for n in xrange(0,11))

这与我们上面的迭代器定义非常相似,只是完整范围预先确定为0到10之间。

我刚刚发现了xrange()(惊讶地发现以前没有看到它...)并将其添加到上面的示例中。 xrange()range()的可迭代版本,具有不预先构建列表的优点。如果您有一个巨大的数据语料库要迭代,并且只有很少的内存可用于执行此操作,那么它将非常有用。


21
自 Python 3.0 起,不再有 xrange() 函数,而新的 range() 函数行为类似于旧的 xrange()。 - user3850
6
在Python 2中,你仍应该使用xrange,因为2to3会自动将其转换。 - Phob

16

这个问题是关于可迭代对象,而不是迭代器。在Python中,序列也是可迭代的,因此让一个可迭代的类表现得像序列一样是一种方法,即给它__getitem____len__方法。我已经在Python 2和3上测试过了。

class CustomRange:

    def __init__(self, low, high):
        self.low = low
        self.high = high

    def __getitem__(self, item):
        if item >= len(self):
            raise IndexError("CustomRange index out of range")
        return self.low + item

    def __len__(self):
        return self.high - self.low


cr = CustomRange(0, 10)
for i in cr:
    print(i)

2
它不必有一个__len__()方法。仅具有预期行为的__getitem__就足够了。 - BlackJack

11

如果你寻找简短而简单的东西,也许这对你来说已经足够了:

class A(object):
    def __init__(self, l):
        self.data = l

    def __iter__(self):
        return iter(self.data)

使用示例:

In [3]: a = A([2,3,4])

In [4]: [i for i in a]
Out[4]: [2, 3, 4]

6

请将以下代码添加到您的类代码中。

 def __iter__(self):
        for x in self.iterable:
            yield x

请确保将self.iterable替换为您要进行迭代的可迭代对象。

以下是示例代码:

class someClass:
    def __init__(self,list):
        self.list = list
    def __iter__(self):
        for x in self.list:
            yield x


var = someClass([1,2,3,4,5])
for num in var: 
    print(num) 

输出

1
2
3
4
5

注意:由于字符串也是可迭代的,所以它们也可以作为类的参数使用。

foo = someClass("Python")
for x in foo:
    print(x)

输出

P
y
t
h
o
n

5

对于包含内置可迭代类型(如strlistsetdict)或任何collections.Iterable实现作为属性的复杂对象,此页面上的所有答案都非常好。在类中,您可以省略某些内容。

class Test(object):
    def __init__(self, string):
        self.string = string

    def __iter__(self):
        # since your string is already iterable
        return (ch for ch in self.string)
        # or simply
        return self.string.__iter__()
        # also
        return iter(self.string)

可以这样使用:

它可用于:

for x in Test("abcde"):
    print(x)

# prints
# a
# b
# c
# d
# e

2
正如你所说,字符串已经是可迭代的了,为什么要在中间加入额外的生成器表达式,而不是直接向字符串请求迭代器(这是生成器表达式内部所做的):return iter(self.string) - BlackJack
@BlackJack 你说得没错。我不知道是什么促使我以那种方式写作。也许我试图避免在解释迭代器语法的工作原理时出现更多的迭代器语法,从而引起任何混淆。 - John Strood

3

这是一个没有yield的可迭代函数。它利用了iter函数和一个闭包,在Python 2中将其状态保留在封闭范围内的可变(list)中。

def count(low, high):
    counter = [0]
    def tmp():
        val = low + counter[0]
        if val < high:
            counter[0] += 1
            return val
        return None
    return iter(tmp, None)

对于Python 3,闭包状态保存在封闭作用域的不可变对象中,并且在本地作用域中使用非局部变量(nonlocal)来更新状态变量。

def count(low, high):
    counter = 0
    def tmp():
        nonlocal counter
        val = low + counter
        if val < high:
            counter += 1
            return val
        return None
    return iter(tmp, None)  

测试;

for i in count(1,10):
    print(i)
1
2
3
4
5
6
7
8
9

1
我总是欣赏巧妙地使用双参数 iter,但需要明确的是:这比仅使用基于 yield 的生成器函数更复杂且效率更低;Python 对于基于 yield 的生成器函数有大量的解释器支持,而您在此无法利用这些支持,从而使得代码显著变慢。尽管如此,我还是给它点了赞。 - ShadowRanger

1
class uc_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
改进之前的 答案,使用 class 的一个优点是你可以添加 __call__ 来返回 self.value 或者甚至 next_value
class uc_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
    def __call__(self):
        next_value = self.value
        self.value += 2
        return next_value

c = uc_iter()
print([c() for _ in range(10)])
print([next(c) for _ in range(5)])
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# [20, 22, 24, 26, 28]

基于Python随机数的另一个示例类,既可以被调用又可以被迭代,可以在我的实现这里中看到。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接