@NoPy: Zur Frage nach den Generatoren: tatsächlich betrifft das Iteratoren. Generatoren sind manche Dinge, die Iteratoren erzeugen. Der Zusammenhang ist dieser. Generatorfunktionen sind Funktionen oder Methoden, die wenigstens ein yield-Statement haben. Generatorausdrücke sehen aus wie List Comprehensions, außer dass sie in runde statt eckige Klammern eingeschlossen sind. Wenn ich eine Generatorfunktion ausführe, erhalte ich als Ergebnis einen Generator zurück. Wenn ich einen Generatorausdruck auswerte, ebenfalls. Generatoren implementieren das Iterator-Protokoll, dh., sie besitzen eine
__iter__() und eine
__next__() Methode (
next() in Python2.x). Generatoren haben zusätzlich noch weitere Methoden:
send() und
close() und
throw(). Objekte, die eine
__iter__() Methode haben, implementieren das Iterable-Protokoll. Wir haben also eine Hierarchie: Iterable <-- Iterator <-- Generator. Das ist keine Vererbungshierarchie, denn wir arbeiten hier mit Duck-Typing, nicht mit Vererbung. Deswegen nennen wir es auch Protokoll, und nicht Basisklasse. Jedes Objekt kann jedes beliebige Protokoll implementieren, wenn der Programmierer das so festlegt (indem er es so programmiert).
Iteratoren haben also genau zwei Methoden:
__iter__() und
__next__(). Die erste ist dafür da, damit man auf einem Iterator
it wieder
iter(it) aufrufen kann. Das braucht man manchmal.
it wird also von
iter() als Iterable verwendet. Die zweite Methode dient zum eigentlichen iterieren. In einer for-Schleife wird sie implizit aufgerufen, aber das kann man auch explizit machen, indem man zB.
elem = next(it) schreibt. Die Funktion next() könnte man, wenn es sie nicht schon gäbe, so implementieren:
Analog dazu ruft
iter(it) einfach
it.__iter__() auf, aber wenn das fehlschlägt, versucht
iter() über den Subscriptionsoperator an die Elemente zu kommen. Das interessiert uns hier aber nicht.
Man verwendet Generatoren bevorzugt dann, wenn das Berechnen der Elemente sehr teuer ist oder die Elemente sehr groß sind. Oder wenn es sehr viele Elemente sind, so dass eine Liste mit allen Elementen sehr viel Speicher benötigen würde. Oder in einem Fall wie diesem: Nehmen wir an, ich wollte einen Iterator haben, der Daten aus einer Netzwerkverbindung liefert, also Daten, die irgendein anderer Computer sendet. Nehmen wir irgendein simples Protokoll an, wo nur Strings gesendet werden, und ein '\n' sei der Trenner zwischen zwei Strings und '\0' signalisiert das Ende der Daten. Nach senden von '\0' wird die Verbindung geschlossen. Ich könnte das so implementieren:
Code: Alles auswählen
class NetIter:
def __init__(self, host, port):
self.host = host
self.port = port
def __iter__(self):
connection = some_connect_function(self.host, self.port)
while True:
data = ''
while True:
data += connection.recv()
if data[-1] == '\n':
yield data[:-1]
break
elif data[-1] == '\0':
yield data[:-1]
return
Iter ist hier als Generatorfunktion implementiert. Angenommen, ich würde sowas machen:
Code: Alles auswählen
netit = NetIter('localhost', 12345)
xs = list(netit)
ys = list(netit)
Würdest du da erwarten, dass xs und ys dieselben Daten enthalten? Wenn nein, warum dann bei openpyxl? Dass die Daten über das Netzwerk kommen, ist dabei übrigens völlig unerheblich. Wichtig ist dagegen, was das Iterator-Protokoll garantiert: dass ein Element nach dem anderen geliefert wird, bis es entweder keine mehr gibt oder keine mehr nachgefragt werden.
Ein völlig anderes Problem ist, was ich mit den gelieferten Elementen anstelle. Sequentiell aus allen Elementen einer Struktur eine neue, evtl. nicht-isomorphe Struktur zu erzeugen, ist ein
left fold. Den kann ich mittels Rekusion erzeugen, wie du das in Dump() getan hast, oder mittels einer for-Schleife (evtl. + Stack) oder mittels reduce(). Einfach ist es, wenn alle Elemente auf dieselbe Weise behandelt werden müssen. Schwieriger, wenn man einen Dispatch durchführen muss. Der Dispatch kann durch Konditionale (if-Statements) durchgeführt werden, aber zB. auch dadurch, dass ich mir Pythons Methoden-Dispatch zunutze mache. Guido nannte das AFAIR mal das Command Dispatch Pattern:
Code: Alles auswählen
class Foo:
pass
class Bar:
pass
class Dispatcher:
def dispatch_Foo(self, elem):
print 'Foo found!', elem
def dispatch_Bar(self, elem):
print 'Bar found!', elem
def dispatch(self, elem):
getattr(self, 'dispatch_' + type(elem).__name__)(elem)
d = Dispatcher()
d.dispatch(Foo())
d.dispatch(Bar())
Das hat aber dasselbe Problem wie explizite if-Abfragen und Visitors. Man muss immer, wenn ein neuer Fall dazukommt, alle Stellen ändern, wo man auf den neuen Fall reagieren muss. Aber das ist kein Problem von Python oder dynamisch typisierten Sprachen, sondern von allen mir bekannten Sprachen. Auch statische Typisierung hilft hier nicht, denn es gibt keine automatische Typprüfung der Art "Oh, hier ist was neues, da generiere ich mal automatisch den Code, den der Programmierer haben möchte, um mit dem neuen Dings umzugehen." Und auch mit Iteratoren hat das nichts zu tun, denn für diese Problemstellung ist es wurst, ob die Daten von einem Iterator geliefert werden oder aus einer Liste oder sonstwo her stammen. Entweder die Elemente haben dasselbe polymorphe Interface, wodurch man den Dispatch geschenkt bekommt, oder man muss den Dispatch selber durchführen, mit den og. Problemen. Manchmal muss man dazu Adapter (welche du Wrapper nennst) schreiben. Auch das hat weder etwas mit dynamischem vs. statischem Typing zu tun, noch mit Iteratoren.
Schlussendlich: mir ist immer noch nicht klar, wo dein Problem liegt. Abgesehen von dem oben gezeigten Command Dispatch Pattern, das so in Bondage & Discipline Sprachen nicht implementierbar ist, hat das alles nichts mit Python zu tun, sondern nur ganz allgemein mit OOP.
In specifications, Murphy's Law supersedes Ohm's.