2016-05-12 8 views
15

Ich habe eine Klasse mit einem __iter__ und einem __len__ Methoden. Letzterer verwendet Ersteres, um alle Elemente zu zählen.Wie list() verbrauchen __iter__ ohne __len__ aufzurufen?

Es funktioniert wie folgt aus:

class A: 
    def __iter__(self): 
     print("iter") 
     for _ in range(5): 
      yield "something" 

    def __len__(self): 
     print("len") 
     n = 0 
     for _ in self: 
      n += 1 
     return n 

Nun, wenn wir zum Beispiel nehmen die Länge einer Instanz druckt len und iter, wie erwartet:

>>> len(A()) 
len 
iter 
5 

Aber wenn wir list() nennen es nennt beide __iter__ und __len__:

>>> list(A()) 
len 
iter 
iter 
['something', 'something', 'something', 'something', 'something'] 

Es funktioniert wie erwartet, wenn wir einen Generator Ausdruck machen :

>>> list(x for x in A()) 
iter 
['something', 'something', 'something', 'something', 'something'] 

ich würde davon ausgehen list(A()) und list(x for x in A()) funktionieren gleich, aber sie nicht.

Beachten Sie, dass es zuerst erscheint __iter__ zu nennen, dann __len__, dann Schleife über den Iterator:

class B: 
    def __iter__(self): 
     print("iter") 

     def gen(): 
      print("gen") 
      yield "something" 

     return gen() 

    def __len__(self): 
     print("len") 
     return 1 

print(list(B())) 

Ausgang:

iter 
len 
gen 
['something'] 

Wie kann ich list() nicht __len__ anrufen damit der Iterator meiner Instanz nicht zweimal verbraucht wird? Ich könnte z.B. a length oder size Methode und man würde dann A().size() anrufen, aber das ist weniger Python.

Ich habe versucht, die Länge in __iter__ und cachen es zu berechnen, so dass nachfolgende Aufrufe __len__ nicht wieder iter müssen aber list() Anrufe __len__ ohne zu wiederholen beginnen so funktioniert es nicht.

Beachten Sie, dass ich in meinem Fall an sehr großen Datensammlungen arbeite, so dass das Zwischenspeichern aller Elemente keine Option ist.

+0

Warum muss __len__ Implementierung __iter__ aufrufen? Empfängt __iter__ bei jedem Aufruf neue Daten? – Daniel

+0

@Daniel Nein, es sind immer die gleichen Daten, aber es muss darüber iterieren, um seine Länge zu erhalten; wir wissen es nicht im voraus. – bfontaine

+0

In welcher Phase von A wissen Sie? auf __init__? auf einer Setter-Methode? – Daniel

Antwort

10

Es ist eine sichere Wette, dass der list() Konstruktor erkennt, dass len() verfügbar ist und es um Aufruf Speicher für die Liste vorbelegt.

Ihre Implementierung ist ziemlich vollständig rückwärts. Sie implementieren __len__() mithilfe von __iter__(), was Python nicht erwartet. Die Erwartung ist, dass len() eine schnelle, effiziente Möglichkeit ist, die Länge im Voraus zu bestimmen.

Ich glaube nicht, dass Sie list(A()) überzeugen können, nicht len zu rufen. Wie Sie bereits festgestellt haben, können Sie einen Zwischenschritt anlegen, der verhindert, dass len aufgerufen wird.

Sie sollten das Ergebnis definitiv zwischenspeichern, wenn die Reihenfolge unveränderlich ist. Wenn es so viele Dinge gibt, wie Sie spekulieren, gibt es keinen Sinn len mehr als einmal zu berechnen.

+0

Danke das macht Sinn. – bfontaine

+0

Anekdote: Ich habe '__len__' als' return len (list (iter (self))) 'implementiert und festgestellt, dass dies eine sehr schlechte Idee war, als mein Test Coverage Tracking nicht mehr funktionierte. "List (foo)" ruft "__len__" auf, was "list()" aufruft, was "__len__" usw. aufruft, bis es einen "MaximumRecursionError" gibt, der das Coverage-Tracking herunterfährt - und dann "list()" unterdrückt Fehler und nimmt an, dass "__len__" nicht verfügbar ist. Langsam und mit unerwarteten Nebenwirkungen! –

+0

Rufen Sie einfach 'sys.setrecursionlimit()' mit einer niedrigen Zahl auf, damit es schneller geht. :-) :-) :-) –

-2

Sie müssen __len__ nicht implementieren.Für eine Klasse, die iterable ist, es muss nur entweder von unten implementieren:

  • __iter__, die ein iterator oder ein generator wie in Ihrer Klasse gibt A & B
  • __getitems__, solange es IndexError erhöht, wenn der Index außerhalb des zulässigen Bereichs

Schlag Code funktioniert immer noch:

class A: 
    def __iter__(self): 
     print("iter") 
     for _ in range(5): 
      yield "something" 

print list(A()) 

Welche Ausgänge:

iter 
['something', 'something', 'something', 'something', 'something'] 
+2

Ich muss * nicht * aber ich möchte in der Lage sein, die Größe meiner Daten zu bekommen, und die Definition '__len__' erlaubt mir,' len (A()) 'anstelle von zB 'len (Liste (A()))'. (ETA: auch 'list (A())' wird nicht funktionieren, wenn ich meine Instanzen Millionen von großen Objekten liefert, muss ich etwas Code schreiben, um den Iterator zu konsumieren und einen Zähler dafür zu erhöhen. '__len__' schien wie ein guter Ort, um das zu tun) – bfontaine

+0

@bfontaine, dann bin ich ein wenig verwirrt mit Ihrer Anforderung. Willst du nur die Größe der Daten, oder auch Iterator/konsumieren die Daten (irgendwann)? –

+0

Beide. Iterieren über die Daten ist die Hauptanforderung, aber in der Lage zu sein, seine Größe direkt zu bekommen wäre eine großartige Ergänzung, die ein paar Tastenanschläge speichern wird, wenn ich mit den Daten in der REPL spiele. – bfontaine

Verwandte Themen