如何覆盖Python列表(迭代器)的行为?

6
运行以下内容:
class DontList(object):
    def __getitem__(self, key):
        print 'Getting item %s' % key
        if key == 10: raise KeyError("You get the idea.")
        return None

    def __getattr__(self, name):
        print 'Getting attr %s' % name
        return None

list(DontList())

生成如下内容:

Getting attr __length_hint__
Getting item 0
Getting item 1
Getting item 2
Getting item 3
Getting item 4
Getting item 5
Getting item 6
Getting item 7
Getting item 8
Getting item 9
Getting item 10
Traceback (most recent call last):
  File "list.py", line 11, in <module>
    list(DontList())
  File "list.py", line 4, in __getitem__
    if key == 10: raise KeyError("You get the idea.")
KeyError: 'You get the idea.'

我该如何更改,以便在仍然允许访问那些键 [1] 等的情况下获得[]
(我尝试过放置 def __length_hint__(self): return 0,但并没有帮助。)
我的真实用例:(供阅读,如果有用请随意忽略此段之后的内容)
在应用某个补丁到iniparse之后,我发现了我的补丁的一个不良副作用。对于我的 Undefined 类设置了 __getattr__,它返回一个新的 Undefined 对象。 不幸的是,这意味着 list(iniconfig.invalid_section)(其中 isinstance(iniconfig, iniparse.INIConfig))会执行以下操作(在__getattr____getitem__ 中放置了简单的print):
Getting attr __length_hint__
Getting item 0
Getting item 1
Getting item 2
Getting item 3
Getting item 4

等等,无限。


请注意,以下答案忽略了 list(x) 的某些方面 - 如果存在,则首先调用 iter 然后调用 __len__,然后将运行迭代器。确保如果 len 有任何副作用(在我的某个对象上,它需要自己运行迭代器才能知道有多少元素),则在完成时重置它。 - Michael Scott Asato Cuthbert
5个回答

7

如果您想覆盖迭代,则只需在您的类中定义__iter__方法即可。


我曾经有点模糊地考虑过这个问题,但最终没有尝试,因为它没有显示“获取attr __iter__”;显然,hasattr或者其他使用方式不是这样的。那么你会推荐什么来产生[] - def __iter__(self): return iter([])(它可以工作,但是这是正确的方法吗)?或者也许我想要通常的TypeError: 'Undefined' object is not iterable- 有没有办法让它以这种方式工作(而不是通过自己产生该错误来欺骗)? - Chris Morgan
通常情况下,它会弹出下一个项目的存储,因此生成器很难返回到先前的状态。您可以始终使用def iter(self): raise TypeError。 - Jakob Bowyer
1
经过思考和Sven的回答后,我认为我想要用return; yield覆盖__iter__而不是在__getitem__中对int引发IndexError,以使Undefined INI部分的行为类似于已定义的部分 - 诸如for section in iniconfigfor key in sectionif section in iniconfigif key in section等操作也应该能够正常工作。 - Chris Morgan
@Chris Morgan:不理解你所说的 def __iter__(self): return; yield 是什么意思。一个 __iter__() 方法需要返回除 None 以外的东西,接着再加上一个 yield 或其他语句都没有作用。也许你应该提交自己的答案或更新你的问题来展示你所谈论的代码。 - martineau
@martineau:这就是意图 - return 在生成器中等同于 raise StopIteration(),这意味着它将产生基本上的 [] - Chris Morgan
显示剩余2条评论

3

只需引发 IndexError 而不是 KeyErrorKeyError 适用于类似映射的类(例如 dict),而 IndexError 适用于序列。

如果在您的类上定义了 __getitem__() 方法,则 Python 将自动从中生成迭代器。 迭代器会在遇到 IndexError 时终止 -- 参见 PEP234


谢谢提供信息,但问题在于“1”是一个完全有效的INI节或值名称。但也许我可以使它对1引发IndexError并接受'1'。不过我认为def __iter__(self): return; yield更加简洁。 - Chris Morgan

3
@Sven所说,那不是应该引发的错误。但问题在于这是有问题的,因为它不是你应该做的事情:防止__getattr__引发AttributeError意味着您已经覆盖了Python用于测试对象是否具有属性的默认方法,并将其替换为新方法(ini_defined(foo.bar))。
但Python已经有了hasattr!为什么不使用它?
>>> class Foo:
...     bar = None
...
>>> hasattr(Foo, "bar")
True
>>> hasattr(Foo, "baz")
False

+1. 我通常不会点赞“不要那样做”的回答,但这是Python并没有明确表示你不应该这样做的情况之一,而且实践中似乎也很少成功。我的直觉是,不要那样做。 - Jason Orendorff
我认为这个API的更改会破坏任何使用hasattr来测试在未定义部分中是否存在键的现有代码。 - Jason Orendorff
需要使用Undefined对象完成的原因是,这样可以在不需要显式创建部分的情况下设置INI值。 这是iniparse的设计,也是正确的。 ini_defined只是我编写的一个简单函数,以便我可以检查是否定义了部分或值。 'bar' in foo也应该管用(实际上更整洁,但在Undefined上它也会出问题,就像list()一样)- 我想我会将所有对ini_defined(foo.bar)的使用更改为'bar' in foo - Chris Morgan
@Chris:这几乎肯定是一个糟糕的设计,尽管我理解你可能不想改变它。'bar' in foo 更好,因为在这种情况下可以覆盖 __getitem__ 方法。 - Katriel
我不认为这是糟糕的设计 - 除了我使用 ini_defined。我不确定为什么之前没有想到使用 x in y;它一直都能很好地工作。 - Chris Morgan

1

通过实现__iter__()方法,覆盖如何迭代您的类。 迭代器通过引发StopIteration异常来通知它们已经完成,这是正常迭代器协议的一部分,不会进一步传播。 下面是将其应用于您的示例类的一种方法:

class DontList(object):
    def __getitem__(self, key):
        print 'Getting item %s' % key
        if key == 10: raise KeyError("You get the idea.")
        return None

    def __iter__(self):
        class iterator(object):
            def __init__(self, obj):
                self.obj = obj
                self.index = -1
            def __iter__(self):
                return self
            def next(self):
                if self.index < 9:
                    self.index += 1
                    return self.obj[self.index]
                else:
                    raise StopIteration

        return iterator(self)

list(DontList())
print 'done'
# Getting item 0
# Getting item 1
# ...
# Getting item 8
# Getting item 9
# done

1
我认为这有些过度了...如果你想这样做(而且OP不想),只需在__getitem__中引发StopIteration而不是KeyError即可。 - Ant
@Ant:也许吧。我写了一个完整的版本,因为我不确定 OP 是想停在 10 还是跳过它。将迭代器作为单独的对象使它更加通用和灵活。 - martineau
不,那只是展示问题没有停止的情况。实际上,def __iter__(self): return; yield 运行得非常好。 - Chris Morgan
+1 是一个直观的例子。然而,它并不是直接使用的。你必须嵌套“重载迭代器”,而在我的问题中,我只需要一个单一的类 - 只需从 __iter__(self) 方法返回一个适当的生成器即可。 - Tomasz Gandor

0

我认为使用return iter([])是正确的方式,但让我们开始思考一下list()的工作原理:

__iter__获取一个元素;如果收到StopIrteration错误,则停止...然后获取该元素。

因此,您只需在__iter__yield一个空生成器,例如(x for x in xrange(0, 0)),或者简单地使用iter([])


iter([]) 可以工作,但过于hacky。return; yield 也有点hacky,因为它滥用了yield,但更整洁。 - Chris Morgan

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