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()被称为中间迭代,则会抛出异常,类似于:

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
4

2 回答 2

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次迭代!

于 2012-09-18T01:35:30.700 回答
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__()在任何情况下何时调用。

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

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

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

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

于 2012-09-18T01:50:44.673 回答