如何为容器对象实现__iter__(self)方法(Python)

159

我写了一个自定义容器对象。

根据这个页面,我需要在我的对象上实现这个方法:

__iter__(self)
然而,在跟进Python参考手册中的迭代器类型链接后,没有给出如何实现自己的示例。
有人可以发布一个代码片段(或链接到资源),展示如何做到这一点吗?
我正在编写的容器是一个映射(即通过唯一键存储值)。 字典可以像这样进行迭代:
for k, v in mydict.items()
在这种情况下,我需要能够在迭代器中返回两个元素(一个元组?)。 尽管已经有几个友好的答案提供了,但仍不清楚如何实现这样的迭代器。能否请有人就如何为类似于字典的容器对象(即表现为字典的自定义类)实现一个迭代器提供更多的指导?
9个回答

175
我通常会使用生成器函数。每次使用yield语句时,它都会将一个项目添加到序列中。
以下代码将创建一个迭代器,首先返回数字5,然后返回some_list中的每个项目。
def __iter__(self):
    yield 5
    yield from some_list

在 Python 3.3 之前,yield from 不存在,所以你必须这样做:
def __iter__(self):
    yield 5
    for x in some_list:
        yield x

1
需要引发StopIteration吗?这如何区分何时停止? - Jonathan
1
some_list中的所有元素都被生成时。 - laike9m
1
实际上,对于这种用例,只有在您希望在“some_list”耗尽之前停止生成值时才需要引发StopIteration。 - Tim Peoples
26
当生成器函数通过你明确调用 return 或者到达函数结尾(和其他所有函数一样,在结尾有一个隐含的 return None)返回时,Python 会自动引发 StopIteration 异常。明确引发 StopIteration 不是必需的,并且在 Python 3.5 中实际上不起作用(请参见 PEP 479): 就像生成器将 return 转换为 StopIteration 一样,它们也会将明确引发的 raise StopIteration 转换为 RuntimeError - Arthur Tacca
这正确吗?我从未见过带有yield的__iter__,我认为应该使用__next__...? - Max

36

另一个选择是从`collections`模块中适当的抽象基类继承,文档见这里

如果容器本身就是迭代器,则可以继承collections.Iterator。然后只需要实现next方法即可。

以下是示例:

>>> from collections import Iterator
>>> class MyContainer(Iterator):
...     def __init__(self, *data):
...         self.data = list(data)
...     def next(self):
...         if not self.data:
...             raise StopIteration
...         return self.data.pop()
...         
...     
... 
>>> c = MyContainer(1, "two", 3, 4.0)
>>> for i in c:
...     print i
...     
... 
4.0
3
two
1

如果你正在查看 collections 模块,可以考虑继承于 SequenceMapping 或其他适合的抽象基类。下面是一个继承于 Sequence 的子类示例:

>>> from collections import Sequence
>>> class MyContainer(Sequence):
...     def __init__(self, *data):
...         self.data = list(data)
...     def __getitem__(self, index):
...         return self.data[index]
...     def __len__(self):
...         return len(self.data)
...         
...     
... 
>>> c = MyContainer(1, "two", 3, 4.0)
>>> for i in c:
...     print i
...     
... 
1
two
3
4.0

NB:感谢Glenn Maynard提醒我需要澄清迭代器与可迭代容器之间的区别。


14
不要混淆可迭代对象和迭代器——如果一个可迭代对象本身不是迭代器(例如容器),则不要从迭代器继承。 - Glenn Maynard
1
@Glenn:你说得对,一个典型的容器并不是迭代器。我只是按照问题所提到的迭代器类型进行回答。我认为从一个更合适的选项继承是更合适的,正如我在答案的结尾所说的那样。我会在答案中澄清这一点。 - Muhammad Alkarouri
似乎使用 .pop() 实现 next 函数会导致一些不良影响。首先,迭代器以相反的顺序返回可迭代对象的元素,其次,在迭代过程中,可迭代对象被耗尽。是否有其他方便的方法? - Dimitri Coukos
通过这个例子,当只定义了__getitem__时,mypy会报错说"MyContainer"没有属性"__iter__"(不可迭代)。如何解决这个问题? - MSH

21
在Python中,“可迭代接口”由两个方法__next__()__iter__()组成。其中,__next__函数最重要,因为它定义了迭代器的行为-即函数确定下一个应返回的值。__iter__()方法用于重置迭代的起始点。通常情况下,当使用__init__()来设置起始点时,__iter__()可以直接返回self。
请参考以下示例代码,定义一个名为“Reverse”的类,实现“可迭代接口”,并对任何序列类的任何实例定义一个迭代器。__next__()方法从序列末尾开始,以序列反向顺序返回值。请注意,实现“序列接口”的类的实例必须定义__len__()__getitem__()方法。
class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, seq):
        self.data = seq
        self.index = len(seq)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

>>> rev = Reverse('spam')
>>> next(rev)   # note no need to call iter()
'm'
>>> nums = Reverse(range(1,10))
>>> next(nums)
9

18
通常情况下,如果你已经定义了next()方法(生成器对象),__iter__()方法只需返回self即可:

这里有一个生成器的虚拟示例:

class Test(object):

    def __init__(self, data):
       self.data = data

    def next(self):
        if not self.data:
           raise StopIteration
        return self.data.pop()

    def __iter__(self):
        return self

但是 __iter__() 还可以像这样使用: http://mail.python.org/pipermail/tutor/2006-January/044455.html


9
这是针对迭代器类的操作,但问题是关于容器对象。 - Glenn Maynard
2
在 Python 3 及以上版本中,这是否仍然是正确的? - Connor
1
"next"方法在名称前后应该有2个下划线。 - Learner

15
如果你的对象包含一组数据,想要将其绑定到对象的迭代器上,可以采用以下方法来实现:
>>> class foo:
    def __init__(self, *params):
           self.data = params
    def __iter__(self):
        if hasattr(self.data[0], "__iter__"):
            return self.data[0].__iter__()
        return self.data.__iter__()
>>> d=foo(6,7,3,8, "ads", 6)
>>> for i in d:
    print i
6
7
3
8
ads
6

3
不要使用hasattr来检查,而是使用try/except AttributeError - IceArdor

8
回答关于“映射”的问题:你提供的__iter__应该迭代映射的。以下是一个简单的示例,创建了一个映射x -> x * x,并在Python3中扩展ABC映射。
import collections.abc

class MyMap(collections.abc.Mapping):
    def __init__(self, n):
        self.n = n

    def __getitem__(self, key): # given a key, return it's value
        if 0 <= key < self.n:
            return key * key
        else:
            raise KeyError('Invalid key')

    def __iter__(self): # iterate over all keys
        for x in range(self.n):
            yield x

    def __len__(self):
        return self.n

m = MyMap(5)
for k, v in m.items():
    print(k, '->', v)
# 0 -> 0
# 1 -> 1
# 2 -> 4
# 3 -> 9
# 4 -> 16

6

如果你不想像其他人建议的那样继承 dict,这里是如何实现一个自定义字典的简单例子中的 __iter__ 方法的直接答案:

class Attribute:
    def __init__(self, key, value):
        self.key = key
        self.value = value

class Node(collections.Mapping):
    def __init__(self):
        self.type  = ""
        self.attrs = [] # List of Attributes

    def __iter__(self):
        for attr in self.attrs:
            yield attr.key

这段代码使用了一个生成器,对其进行了详细描述(点击此处查看)

由于我们从Mapping中继承而来,因此您还需要实现__getitem____len__方法:

    def __getitem__(self, key):
        for attr in self.attrs:
            if key == attr.key:
                return attr.value
        raise KeyError

    def __len__(self):
        return len(self.attrs)

3

在某些情况下可能适用的一种选项是将您的自定义类继承dict。如果它像字典一样工作,那么使用字典似乎是一个合理的选择; 也许它应该成为一个字典。这样,您就可以免费获得类似于字典的迭代。

class MyDict(dict):
    def __init__(self, custom_attribute):
        self.bar = custom_attribute

mydict = MyDict('Some name')
mydict['a'] = 1
mydict['b'] = 2

print mydict.bar
for k, v in mydict.items():
    print k, '=>', v

输出:

Some name
a => 1
b => 2

2

从字典继承并修改其iter的示例,例如,在for循环中跳过键2

# method 1
class Dict(dict):
    def __iter__(self):
        keys = self.keys()
        for i in keys:
            if i == 2:
                continue
            yield i

# method 2
class Dict(dict):
    def __iter__(self):
        for i in super(Dict, self).__iter__():
            if i == 2:
                continue
            yield i

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