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

9 浏览
0 Comments

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

我如何在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更一般地说,迭代器应该能够控制值的来源,甚至可以即时计算值(而不考虑实例的任何特定属性)。

0
0 Comments

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

在讨论中,某些情况下在__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异常。

0
0 Comments

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

构建迭代器的四种方法有:

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方法。

以上是关于如何构建基本迭代器的内容。这四种方法各有优劣,选择哪种方法主要取决于个人偏好和具体需求。

0
0 Comments

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

迭代器对象在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循环执行之前。

0