【问题标题】:Prevent modification of custom class while iterating防止在迭代时修改自定义类
【发布时间】:2012-09-10 06:00:28
【问题描述】:

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

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

【问题讨论】:

  • 你们是如何实现next的?
  • 如果foo 是dict,您可以使用for k in foo.keys()[:]。在其他情况下,它取决于next 方法的实现。
  • @DavidRobinson 我不是——这只是一个界面。我想知道您如何为具有任意数据结构的任意类执行此操作,因为我认为这种情况相当普遍。

标签: python iterator concurrentmodification


【解决方案1】:

您不应将迭代器与实例混用。否则,当您想一次多次迭代实例时会发生什么?

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

将迭代器拆分为一个单独的类。创建迭代器实例时存储对象的大小。每当调用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 次迭代!

【讨论】:

    【解决方案2】:

    如果项目是可索引的并且有长度,你可以这样做,类似于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__() 何时会无论如何都会被调用。

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

    在任何情况下你都不能只使用self 作为你的迭代器。

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

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

    【讨论】:

    • 我特别喜欢这种方法可以检测到容器对象长度的 任何 更改(即即使 OP 没有该操作的接口也会删除在他们的示例代码中)。唯一的问题可能是它似乎假设容器是可索引的。
    • 当然,如果您在两次迭代之间添加和删除一个项目,这不会捕获...
    • 关于实时迭代器想法的实现:我认为您犯的错误与我在答案中所做的相同(我已删除),因为您假设生成器将是消耗到最后。我不完全确定,但似乎在放弃交互器的情况下,_itercount 不会正确减少。
    • 是的,这个例子有这个缺陷。这就是为什么我说“要获得奖励积分,请实施'__del__()'”。您需要编写自己的迭代器类,我不想调试。 :-)
    • 是的,我想这基本上就是@gnibbler 在他的回答中所说的(创建一个单独的迭代器类)。如果我有时间并且觉得很有动力,我可能会试一试……毕竟这看起来像是一个基本的迭代器问题,需要遵循一个配方或模式。
    猜你喜欢
    • 2011-12-30
    • 2021-12-31
    • 2013-05-13
    • 2014-09-17
    • 2017-08-08
    • 2016-03-26
    • 2017-12-05
    • 2018-05-28
    • 1970-01-01
    相关资源
    最近更新 更多