3 Stimmen

Verhinderung der Änderung einer benutzerdefinierten Klasse während der Iteration

Wenn ich eine Klasse mit der Schnittstelle habe:

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

...wie würde ich die Dinge so einrichten, dass, wenn add() in der Mitte der Iteration aufgerufen wird, wird eine Ausnahme ausgelöst, ähnlich wie bei:

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

Aktualisierung: Wenn die Schnittstelle weitere Methoden benötigt, können Sie diese gerne hinzufügen. Ich habe auch die Implementierung von __iter__() .

Update #2 Auf der Grundlage der Antwort von kindall habe ich die folgende Pseudo-Implementierung entworfen. Beachten Sie, dass _datastruture und die zugehörigen Methoden, die darauf verweisen, Abstraktionen sind und der Klassenschreiber seine eigenen Datenstruktur-Traversal- und Positionszeiger-Mechanismen schreiben müsste.

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

Aktualisierung 3 Nachdem ich noch mehr gelesen habe, scheint es __del__ ist wahrscheinlich nicht der richtige Weg. Die folgende Lösung könnte besser sein, obwohl sie vom Benutzer verlangt, einen nicht erschöpften Iterator explizit freizugeben.

    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

3voto

John La Rooy Punkte 278961

Sie sollten den Iterator nicht mit der Instanz vermischen. Was passiert sonst, wenn Sie die Instanz mehr als einmal auf einmal iterieren wollen?

Überlegen Sie, wo Sie die Position des Iterators speichern wollen.

Aufteilung des Iterators in eine eigene Klasse. Speichern Sie die Größe des Objekts, wenn Sie die Iteratorinstanz erstellen. Überprüfen Sie die Größe immer dann, wenn next() heißt

dicts sind auch nicht narrensicher. Sie können einen Schlüssel hinzufügen und entfernen, was die Iteration durcheinander bringt, aber nicht zu einem Fehler führt

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}

Weit mehr als 3 Iterationen!

1voto

kindall Punkte 167554

Wenn das Element indizierbar ist und eine Länge hat, können Sie etwas wie folgt tun, was ähnlich ist wie dict tut es:

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

Ein Nachteil ist, wenn der Aufrufer mehrere Änderungen vornimmt, die zu keiner Nettoänderung der Länge führen (z. B. Hinzufügen und anschließendes Löschen eines Elements), wird dies nicht erfasst. Natürlich könnten Sie einen Revisionszähler verwenden (der jedes Mal erhöht wird, wenn eine andere Methode eine Änderung vornimmt), anstatt nur die Länge zu überprüfen:

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

Das wird unübersichtlich, da Sie den Revisionszähler in jeder Methode, die die Liste ändern kann, erhöhen müssen. Man könnte eine Metaklasse oder einen Klassendekorator schreiben, um automatisch solche Wrapper-Methoden für eine Liste zu schreiben.

Ein anderer Ansatz wäre, eine Zählung der "lebenden" Iteratoren zu führen, indem ein Instanzattribut inkrementiert wird, wenn ein Iterator erstellt wird, und dekrementiert wird, wenn er erschöpft ist. Dann wird in add() prüfen Sie, ob dieses Attribut Null ist, und lösen andernfalls eine Ausnahme aus.

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)

Bonuspunkte erhalten Sie, wenn Sie __del__() auf den Iterator, so dass der Zähler auch dekrementiert wird, wenn ein Objekt den Bereich verlässt, ohne erschöpft zu sein. (Vorsicht vor doppelter Dekrementierung!) Dies erfordert die Definition einer eigenen Iterator-Klasse, anstatt diejenige zu verwenden, die Python Ihnen gibt, wenn Sie yield in einer Funktion, und es gibt natürlich keine Garantie dafür, wann __del__() wird in jedem Fall aufgerufen.

Leider können Sie nicht wirklich jemanden daran hindern, den "Schutz", den Sie hinzufügen, zu umgehen. Wir sind hier alle mündige Erwachsene.

Was Sie auf keinen Fall tun können, ist, einfach die self als Ihren Iterator.

Endlich, Hier ein Beispiel eines anderen, mehr oder weniger entgegengesetzten Ansatzes: Sie lassen den Aufrufer die Änderungen vornehmen, verschieben aber die tatsächliche Anwendung, bis die Iteration abgeschlossen ist. Ein Kontextmanager wird verwendet, um die Änderungen explizit abzuschließen.

Um sicherzustellen, dass die Aufrufer den Kontextmanager verwenden, könnten Sie die Iteration verweigern, wenn Sie sich nicht in einem Kontext befinden (z. B. Check in __iter__() ein Kennzeichen, das in __enter__() ), speichern Sie dann eine Liste der Iterator-Objekte und machen Sie sie ungültig, wenn Sie den Kontext verlassen (z.B. setzen Sie ein Flag in jedem Iterator, so dass er bei der nächsten Iteration eine Ausnahme auslöst).

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X