2012-09-18 149 views
3

如果我与接口的类:防止修改而迭代

class AnIteratable(object): 

    def __init__(self): 
    #initialize data structure 

    def add(self, obj): 
    # add object to data structure 

    def __iter__(self): 
    #return the iterator 

    def next(self): 
    # return next object 

...我将如何设置的东西,所以如果add()被称为中间迭代的异常thown,类似于:

In [14]: foo = {'a': 1} 

In [15]: for k in foo: 
    ....:  foo[k + k] = 'ohnoes' 
    ....:  
--------------------------------------------------------------------------- 
RuntimeError        Traceback (most recent call last) 
<ipython-input-15-2e1d338a456b> in <module>() 
----> 1 for k in foo: 
     2  foo[k + k] = 'ohnoes' 
     3 

RuntimeError: dictionary changed size during iteration 

更新: 如果接口需要更多的方法,随意添加他们。我也删除了执行__iter__()

更新#2 基于kindall的回答,我嘲笑了下面的伪代码实现。请注意,_datastruture和索引到它的相关方法是抽象类,类作者必须编写他/她自己的数据结构遍历和位置指针机制。

class AnIteratable(object): 

    def __init__(self): 
    self._itercount = 0 
    self._datastructure = init_data_structure() #@UndefinedVariable 
    # _datastructure, and the methods called on it, are abstractions. 

    def add(self, obj): 
    if self._itercount: 
     raise RuntimeError('Attempt to change object while iterating') 
    # add object to data structure 

    def __iter__(self): 
    self._itercount += 1 
    return self.AnIterator(self) 

    class AnIterator(object): 

    def __init__(self, aniterable): 
     self._iterable = aniterable 
     self._currentIndex = -1 #abstraction 
     self._notExhausted = True 

    def next(self): 
     if self._iterable._datastructure.hasNext(self._currentIndex): 
     self._currentIndex += 1 
     return self._iterable._datastructure.next(self._currentIndex) 
     else: 
     if self._notExhausted: 
      self._iterable._itercount -= 1 
     self._notExhausted = False 
     raise StopIteration 

    def __next__(self): 
     return self.next() 

    # will be called when there are no more references to this object 
    def __del__(self): 
     if self._notExhausted: 
     self._iterable._itercount -= 1 

更新3 阅读一些后,似乎__del__可能不是正确的道路要走。以下可能是一个更好的解决方案,但它需要用户明确地发布一个未耗尽的迭代器。

def next(self): 
     if self._notExhausted and 
       self._iterable._datastructure.hasNext(self._currentIndex): 
     #same as above from here 

    def discard(self): 
     if self._notExhausted: 
     self._ostore._itercount -= 1 
     self._notExhausted = False 
+0

你是如何实现'next'的? –

+0

如果'foo'是一个'dict',你可以在'foo.keys()[:]'中使用'for k。在其他情况下,它取决于'next'方法的实现。 –

+0

@DavidRobinson我不是 - 这只是一个界面。我想知道如何为具有任意数据结构的任意类做这件事,因为我认为这种情况相当普遍。 – elhefe

回答

3

您不应该将迭代器与实例混合使用。否则,如果您想一次遍历实例多次,会发生什么情况?

想想你在哪里存储迭代器的位置。

将迭代器拆分为单独的类。在创建迭代器实例时存储对象的大小。检查大小,每当next()被称为

dicts也不是万无一失。你可以添加和删除一个密钥,这将重复迭代,但不会抛出错误

Python 2.7.3 (default, Aug  1 2012, 05:14:39)  
[GCC 4.6.3] on linux2 
Type "help", "copyright", "credits" or "license" for more information. 
>>> d = {i:i for i in range(3)} 
>>> d 
{0: 0, 1: 1, 2: 2} 
>>> for k in d: 
...  d[k+3] = d.pop(k) 
...  print d 
... 
{1: 1, 2: 2, 3: 0} 
{2: 2, 3: 0, 4: 1} 
{3: 0, 4: 1, 5: 2} 
{4: 1, 5: 2, 6: 0} 
{5: 2, 6: 0, 7: 1} 
{6: 0, 7: 1, 8: 2} 
{7: 1, 8: 2, 9: 0} 
{8: 2, 9: 0, 10: 1} 
{9: 0, 10: 1, 11: 2} 
{10: 1, 11: 2, 12: 0} 
{11: 2, 12: 0, 13: 1} 
{12: 0, 13: 1, 14: 2} 
{13: 1, 14: 2, 15: 0} 
{16: 1, 14: 2, 15: 0} 
{16: 1, 17: 2, 15: 0} 
{16: 1, 17: 2, 18: 0} 

超过3次迭代!

1

如果该项目是可转位的,其长度,你可以做这样的事情,这是类似dict是怎么做的:

class AnIterable(list): 

    def __iter__(self): 
     n = len(self) 
     i = 0 
     while i < len(self): 
      if len(i) != n: 
       raise RuntimeError("object changed size during iteration") 
      yield self[i] 
      i += 1 

一个缺点是,如果主叫方使得导致多种变化不净更改为长度(例如添加,然后删除一个元素)它不会被捕获。当然,你可以使用一个版本计数器(递增当有一些其他的方法进行了更改),而不是只检查长度:

class AnIterable(object): 

    def __init__(self, iterable=()): 
     self._content = list(iterable) 
     self._rev = 0 

    def __iter__(self): 
     r = self._rev 
     for x in self._content: 
      if self._rev != r: 
       raise RuntimeError("object changed during iteration") 
      yield x 

    def add(self, item): 
     self._content.append(item) 
     self._rev += 1 

这就会变得混乱,因为你必须增加在每个方法的修订计数器可以修改名单。我猜可以编写一个元类或类装饰器来自动为列表编写这样的包装器方法。

另一种方法是保持“实时”迭代器的计数,在创建迭代器时递增实例属性,并在耗尽时递减实例属性。然后在add()中,检查以确保此属性为零,如果不是,则引发异常。

class AnIterable(object): 

    def __init__(self, iterable=()): 
     self._itercount = 0 
     self._content = list(iterable) 

    def __iter__(self): 
     self._itercount += 1 
     try: 
      for x in self._content: 
       yield x 
     finally: 
      self._itercount -= 1 

    def add(self, obj): 
     if self._itercount: 
      raise RuntimeError("cannot change object while iterating") 
     self._content.append(obj) 

奖励积分,实现对迭代器__del__()所以当一个对象超出范围而不被耗尽数也减少。 (注意双重递减!)这将需要定义自己的自定义迭代器类,而不是在函数中使用yield时使用的Python给出的一个类,并且当然不能保证在什么时候__del__()会被调用案件。

唉,你不能真的阻止某人避开你添加的任何“保护”。我们都在这里同意大人。

你在任何情况下都无法做到的只是使用self作为迭代器。

最后,here's an example或多或少有相反的方法:你让调用者进行更改,但实际上应用它们直到迭代完成。上下文管理器用于明确地完成更改。

为了确保呼叫者使用上下文管理器,你可以拒绝重复,如果你在一个范围内(例如,在__iter__()检查标志__enter__()设置),然后存储迭代器对象的列表和它们无效不是退出上下文时(例如,在每个迭代器中设置一个标志,以便在下一次迭代时引发异常)。

+0

我特别喜欢这种方法会检测容器对象长度的任何变化(即,即使OP在其示例代码中没有该操作的接口),也会删除。唯一的问题可能是它似乎认为容器是可索引的。 – martineau

+0

当然,如果你在迭代之间添加和删除一个项目,这将无法捕捉到... – kindall

+0

关于实时迭代器思想的实现:我认为你犯了同样的错误,我在答案中做了因为你假设发电机将被消耗直到结束。我并不完全确定,但看起来在一个interator被抛弃的情况下,'_itercount'不会被正确地减少。 – martineau