在迭代时防止修改自定义类

时间:2012-09-18 01:09:04

标签: python iterator concurrentmodification

如果我有一个带接口的类:

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()时会发生异常,类似于:

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

2 个答案:

答案 0 :(得分: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 :(得分: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__(),这样当对象超出范围而不会耗尽时,计数也会减少。 (注意双递减!)这将需要定义你自己的自定义迭代器类,而不是使用Python在函数中使用yield时给你的那个,当然不能保证何时{{在任何情况下都会被调用。

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

在任何情况下你不能做的只是使用__del__()作为你的迭代器。

最后,here's an example采用不同的,或多或少相反的方法:让调用者进行更改,但推迟实际应用它们直到迭代完成。上下文管理器用于显式完成更改。

为了确保调用者使用上下文管理器,如果您不在上下文中(例如,在self中检查__iter__()一个标志),则可以拒绝迭代,然后存储列表迭代器对象并在退出上下文时使它们无效(例如,在每个迭代器中设置一个标志,以便在下一次迭代时引发异常)。