Functional Programming HOWTO¶
- Autor:
A. M. Kuchling
- Version:
0.32
In diesem Dokument werden wir Pythons Funktionen, die sich für die Implementierung von Programmen im funktionalen Stil eignen, näher betrachten. Nach einer Einführung in die Konzepte der funktionalen Programmierung befassen wir uns mit Sprachmerkmalen wie Iteratoren und Generatoren sowie relevanten Bibliotheksmodulen wie itertools und functools.
Einleitung¶
Dieser Abschnitt erklärt das Grundkonzept der funktionalen Programmierung. Wenn Sie nur an Pythons Sprachmerkmalen interessiert sind, überspringen Sie diesen Abschnitt und gehen Sie zum nächsten Abschnitt über Iteratoren.
Programmiersprachen unterstützen die Zerlegung von Problemen auf verschiedene Arten.
Die meisten Programmiersprachen sind **prozedural**: Programme sind Listen von Anweisungen, die dem Computer sagen, was er mit der Eingabe des Programms tun soll. C, Pascal und sogar Unix-Shells sind prozedurale Sprachen.
In **deklarativen** Sprachen schreiben Sie eine Spezifikation, die das zu lösende Problem beschreibt, und die Sprachimplementierung ermittelt, wie die Berechnung effizient durchgeführt werden soll. SQL ist die deklarative Sprache, mit der Sie wahrscheinlich am vertrautesten sind; eine SQL-Abfrage beschreibt den Datensatz, den Sie abrufen möchten, und die SQL-Engine entscheidet, ob Tabellen gescannt oder Indizes verwendet werden sollen, welche Unterklauseln zuerst ausgeführt werden sollen usw.
**Objektorientierte** Programme manipulieren Sammlungen von Objekten. Objekte haben einen internen Zustand und unterstützen Methoden, die diesen internen Zustand auf irgendeine Weise abfragen oder modifizieren. Smalltalk und Java sind objektorientierte Sprachen. C++ und Python sind Sprachen, die objektorientierte Programmierung unterstützen, aber die Verwendung objektorientierter Merkmale nicht erzwingen.
**Funktionale** Programmierung zerlegt ein Problem in eine Reihe von Funktionen. Idealerweise nehmen Funktionen nur Eingaben und erzeugen Ausgaben und haben keinen internen Zustand, der die für eine gegebene Eingabe erzeugte Ausgabe beeinflusst. Bekannte funktionale Sprachen sind die ML-Familie (Standard ML, OCaml und andere Varianten) und Haskell.
Die Designer einiger Computersprachen entscheiden sich dafür, einen bestimmten Programmieransatz hervorzuheben. Dies erschwert oft das Schreiben von Programmen, die einen anderen Ansatz verwenden. Andere Sprachen sind Mehrzweck-Programmiersprachen, die mehrere verschiedene Ansätze unterstützen. Lisp, C++ und Python sind Mehrzweck-Sprachen. Sie können Programme oder Bibliotheken schreiben, die in all diesen Sprachen weitgehend prozedural, objektorientiert oder funktional sind. In einem großen Programm können verschiedene Abschnitte mit unterschiedlichen Ansätzen geschrieben werden; die GUI könnte objektorientiert sein, während die Verarbeitungslogik prozedural oder funktional ist, zum Beispiel.
In einem funktionalen Programm fließt die Eingabe durch eine Reihe von Funktionen. Jede Funktion arbeitet auf ihrer Eingabe und erzeugt eine Ausgabe. Der funktionale Stil entmutigt Funktionen mit Nebeneffekten, die den internen Zustand ändern oder andere Änderungen vornehmen, die in der Rückgabe der Funktion nicht sichtbar sind. Funktionen ohne Nebeneffekte werden als **rein funktional** bezeichnet. Das Vermeiden von Nebeneffekten bedeutet, keine Datenstrukturen zu verwenden, die sich während der Ausführung eines Programms ändern. Die Ausgabe jeder Funktion muss nur von ihrer Eingabe abhängen.
Einige Sprachen sind bei der Reinheit sehr streng und haben nicht einmal Zuweisungsanweisungen wie a=3 oder c = a + b. Es ist jedoch schwierig, alle Nebeneffekte zu vermeiden, wie z. B. die Ausgabe auf dem Bildschirm oder das Schreiben auf eine Festplattendatei. Ein weiteres Beispiel ist ein Aufruf der Funktion print() oder time.sleep(), von denen keine einen nützlichen Wert zurückgibt. Beide werden nur wegen ihrer Nebeneffekte aufgerufen, Text auf den Bildschirm zu senden oder die Ausführung eine Sekunde lang zu pausieren.
Python-Programme im funktionalen Stil werden normalerweise nicht bis zum Extrem getrieben, um alle I/O-Operationen oder alle Zuweisungen zu vermeiden. Stattdessen bieten sie eine funktional erscheinende Schnittstelle, verwenden aber intern nicht-funktionale Features. Zum Beispiel verwendet die Implementierung einer Funktion immer noch Zuweisungen an lokale Variablen, aber sie ändert keine globalen Variablen und hat keine anderen Nebeneffekte.
Funktionale Programmierung kann als das Gegenteil von objektorientierter Programmierung betrachtet werden. Objekte sind kleine Kapseln, die einen internen Zustand und eine Sammlung von Methodenaufrufen enthalten, mit denen Sie diesen Zustand ändern können, und Programme bestehen darin, die richtige Menge an Zustandsänderungen vorzunehmen. Funktionale Programmierung möchte Zustandsänderungen so weit wie möglich vermeiden und arbeitet mit Daten, die zwischen Funktionen fließen. In Python können Sie die beiden Ansätze kombinieren, indem Sie Funktionen schreiben, die Instanzen annehmen und zurückgeben, die Objekte in Ihrer Anwendung darstellen (E-Mails, Transaktionen usw.).
Funktionelles Design mag wie eine seltsame Einschränkung erscheinen. Warum sollten Sie Objekte und Nebeneffekte vermeiden? Es gibt theoretische und praktische Vorteile des funktionalen Stils:
Formale Beweisbarkeit.
Modularität.
Komponierbarkeit.
Einfache Fehlersuche und Tests.
Formale Beweisbarkeit¶
Ein theoretischer Vorteil ist, dass es einfacher ist, einen mathematischen Beweis für die Korrektheit eines funktionalen Programms zu konstruieren.
Seit langem sind Forscher daran interessiert, Wege zu finden, Programme mathematisch korrekt zu beweisen. Dies unterscheidet sich vom Testen eines Programms mit zahlreichen Eingaben und dem Schluss, dass seine Ausgabe normalerweise korrekt ist, oder vom Lesen des Quellcodes eines Programms und dem Schluss, dass der Code richtig aussieht. Das Ziel ist stattdessen ein rigoroser Beweis, dass ein Programm für alle möglichen Eingaben das richtige Ergebnis liefert.
Die Technik zum Beweisen der Korrektheit von Programmen besteht darin, **Invarianten** aufzuschreiben, Eigenschaften der Eingabedaten und der Variablen des Programms, die immer wahr sind. Für jede Codezeile zeigen Sie dann, dass, wenn die Invarianten X und Y **vor** der Ausführung der Zeile wahr sind, die leicht unterschiedlichen Invarianten X' und Y' **nach** der Ausführung der Zeile wahr sind. Dies wird fortgesetzt, bis Sie das Ende des Programms erreichen, an dem die Invarianten den gewünschten Bedingungen für die Ausgabe des Programms entsprechen sollten.
Die Vermeidung von Zuweisungen in der funktionalen Programmierung entstand, weil Zuweisungen mit dieser Technik schwer zu handhaben sind; Zuweisungen können Invarianten brechen, die vor der Zuweisung wahr waren, ohne neue Invarianten zu erzeugen, die weitergegeben werden können.
Leider ist das Beweisen von Programmen weitgehend unpraktisch und für Python-Software nicht relevant. Selbst triviale Programme erfordern Beweise, die mehrere Seiten lang sind; der Korrektheitsbeweis für ein mäßig komplexes Programm wäre enorm, und wenige oder keine der Programme, die Sie täglich verwenden (der Python-Interpreter, Ihr XML-Parser, Ihr Webbrowser), könnten als korrekt bewiesen werden. Selbst wenn Sie einen Beweis aufgeschrieben oder generiert hätten, gäbe es dann die Frage der Verifizierung des Beweises; vielleicht gibt es einen Fehler darin, und Sie glauben fälschlicherweise, das Programm als korrekt bewiesen zu haben.
Modularität¶
Ein praktischerer Vorteil der funktionalen Programmierung ist, dass sie Sie zwingt, Ihr Problem in kleine Teile zu zerlegen. Programme sind dadurch modularer. Es ist einfacher, eine kleine Funktion zu spezifizieren und zu schreiben, die eine Sache tut, als eine große Funktion, die eine komplizierte Transformation durchführt. Kleine Funktionen sind auch leichter zu lesen und auf Fehler zu prüfen.
Einfache Fehlersuche und Tests¶
Das Testen und Debuggen eines funktionalen Programms ist einfacher.
Die Fehlersuche wird vereinfacht, da Funktionen im Allgemeinen klein und klar spezifiziert sind. Wenn ein Programm nicht funktioniert, ist jede Funktion ein Schnittstellenpunkt, an dem Sie überprüfen können, ob die Daten korrekt sind. Sie können die Zwischeneingaben und -ausgaben betrachten, um schnell die Funktion zu isolieren, die für einen Fehler verantwortlich ist.
Tests sind einfacher, da jede Funktion ein potenzielles Subjekt für einen Unit-Test ist. Funktionen hängen nicht vom Systemzustand ab, der vor der Ausführung eines Tests repliziert werden muss; stattdessen müssen Sie nur die richtige Eingabe synthetisieren und dann überprüfen, ob die Ausgabe den Erwartungen entspricht.
Komponierbarkeit¶
Bei der Arbeit an einem funktionalen Programm schreiben Sie eine Reihe von Funktionen mit unterschiedlichen Ein- und Ausgaben. Einige dieser Funktionen sind zwangsläufig auf eine bestimmte Anwendung spezialisiert, andere sind jedoch in einer Vielzahl von Programmen nützlich. Zum Beispiel kann eine Funktion, die einen Verzeichnispfad nimmt und alle XML-Dateien im Verzeichnis zurückgibt, oder eine Funktion, die einen Dateinamen nimmt und seinen Inhalt zurückgibt, in vielen verschiedenen Situationen angewendet werden.
Mit der Zeit werden Sie eine persönliche Bibliothek mit Dienstprogrammen aufbauen. Oft werden Sie neue Programme erstellen, indem Sie bestehende Funktionen in einer neuen Konfiguration anordnen und einige für die aktuelle Aufgabe spezialisierte Funktionen schreiben.
Iteratoren¶
Ich beginne mit einem Python-Sprachmerkmal, das eine wichtige Grundlage für das Schreiben von Programmen im funktionalen Stil darstellt: Iteratoren.
Ein Iterator ist ein Objekt, das einen Datenstrom repräsentiert; dieses Objekt gibt die Daten einzeln zurück. Ein Python-Iterator muss eine Methode namens __next__() unterstützen, die keine Argumente nimmt und immer das nächste Element des Stroms zurückgibt. Wenn keine weiteren Elemente im Strom vorhanden sind, muss __next__() die Ausnahme StopIteration auslösen. Iteratoren müssen jedoch nicht endlich sein; es ist durchaus sinnvoll, einen Iterator zu schreiben, der einen unendlichen Datenstrom erzeugt.
Die eingebaute Funktion iter() nimmt ein beliebiges Objekt und versucht, einen Iterator zurückzugeben, der den Inhalt oder die Elemente des Objekts zurückgibt, und löst bei Objekten, die keine Iteration unterstützen, eine TypeError aus. Mehrere von Pythons integrierte Datentypen unterstützen die Iteration, die gebräuchlichsten sind Listen und Wörterbücher. Ein Objekt wird als **Iterable** bezeichnet, wenn Sie einen Iterator dafür erhalten können.
Sie können die Iterationsschnittstelle manuell ausprobieren.
>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it
<...iterator object at ...>
>>> it.__next__() # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
Python erwartet Iterable in mehreren verschiedenen Kontexten, am wichtigsten ist die for-Anweisung. In der Anweisung for X in Y muss Y ein Iterator oder ein Objekt sein, für das iter() einen Iterator erstellen kann. Diese beiden Anweisungen sind gleichwertig.
for i in iter(obj):
print(i)
for i in obj:
print(i)
Iteratoren können mithilfe der Konstruktorfunktionen list() oder tuple() als Listen oder Tupel materialisiert werden.
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)
Sequenz-Entpackung unterstützt ebenfalls Iteratoren: Wenn Sie wissen, dass ein Iterator N Elemente zurückgibt, können Sie sie in ein N-Tupel entpacken.
>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)
Eingebaute Funktionen wie max() und min() können ein einzelnes Iterator-Argument nehmen und geben das größte bzw. kleinste Element zurück. Die Operatoren "in" und "not in" unterstützen ebenfalls Iteratoren: X in iterator ist wahr, wenn X im vom Iterator zurückgegebenen Strom gefunden wird. Sie werden offensichtliche Probleme haben, wenn der Iterator endlich ist; max() und min() werden niemals zurückkehren, und wenn das Element X nie im Strom erscheint, werden die Operatoren "in" und "not in" ebenfalls nicht zurückkehren.
Beachten Sie, dass Sie in einem Iterator nur vorwärts gehen können; es gibt keine Möglichkeit, das vorherige Element abzurufen, den Iterator zurückzusetzen oder eine Kopie davon zu erstellen. Iterator-Objekte können optional diese zusätzlichen Funktionen bereitstellen, aber das Iterator-Protokoll spezifiziert nur die Methode __next__(). Funktionen können daher die gesamte Ausgabe des Iterators verbrauchen, und wenn Sie etwas anderes mit demselben Strom tun müssen, müssen Sie einen neuen Iterator erstellen.
Datentypen, die Iteratoren unterstützen¶
Wir haben bereits gesehen, wie Listen und Tupel Iteratoren unterstützen. Tatsächlich unterstützt jeder Python-Sequenztyp, wie z. B. Zeichenketten, automatisch die Erstellung eines Iterators.
Das Aufrufen von iter() für ein Wörterbuch gibt einen Iterator zurück, der über die Schlüssel des Wörterbuchs iteriert.
>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
... 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
... print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12
Beachten Sie, dass die Iterationsreihenfolge von Wörterbüchern ab Python 3.7 garantiert der Einfügungsreihenfolge entspricht. In früheren Versionen war das Verhalten undefiniert und konnte zwischen Implementierungen variieren.
Das Anwenden von iter() auf ein Wörterbuch iteriert immer über die Schlüssel, aber Wörterbücher haben Methoden, die andere Iteratoren zurückgeben. Wenn Sie über Werte oder Schlüssel/Wert-Paare iterieren möchten, können Sie explizit die Methoden values() oder items() aufrufen, um einen entsprechenden Iterator zu erhalten.
Der Konstruktor dict() kann einen Iterator akzeptieren, der einen endlichen Strom von (key, value)-Tupeln zurückgibt.
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}
Dateien unterstützen auch die Iteration, indem die Methode readline() aufgerufen wird, bis keine Zeilen mehr in der Datei vorhanden sind. Das bedeutet, dass Sie jede Zeile einer Datei wie folgt lesen können:
for line in file:
# do something for each line
...
Mengen können ihre Inhalte aus einem Iterable aufnehmen und Ihnen erlauben, über die Elemente der Menge zu iterieren.
>>> S = {2, 3, 5, 7, 11, 13}
>>> for i in S:
... print(i)
2
3
5
7
11
13
Generator-Ausdrücke und Listen-Abstraktionen¶
Zwei gängige Operationen auf der Ausgabe eines Iterators sind 1) die Durchführung einer Operation für jedes Element und 2) die Auswahl einer Teilmenge von Elementen, die eine Bedingung erfüllen. Zum Beispiel möchten Sie möglicherweise aus einer Liste von Zeichenketten nachgestellte Leerzeichen von jeder Zeile entfernen oder alle Zeichenketten extrahieren, die einen bestimmten Teilstring enthalten.
Listen-Abstraktionen und Generator-Ausdrücke (Kurzform: „listcomps“ und „genexps“) sind eine prägnante Notation für solche Operationen, entlehnt aus der funktionalen Programmiersprache Haskell (https://www.haskell.org/). Sie können alle Leerzeichen aus einem Zeichenstrom entfernen mit dem folgenden Code:
>>> line_list = [' line 1\n', 'line 2 \n', ' \n', '']
>>> # Generator expression -- returns iterator
>>> stripped_iter = (line.strip() for line in line_list)
>>> # List comprehension -- returns list
>>> stripped_list = [line.strip() for line in line_list]
Sie können nur bestimmte Elemente auswählen, indem Sie eine "if"-Bedingung hinzufügen.
>>> stripped_list = [line.strip() for line in line_list
... if line != ""]
Mit einer Listen-Abstraktion erhalten Sie eine Python-Liste; stripped_list ist eine Liste, die die resultierenden Zeilen enthält, kein Iterator. Generator-Ausdrücke geben einen Iterator zurück, der die Werte bei Bedarf berechnet, ohne alle Werte auf einmal materialisieren zu müssen. Das bedeutet, dass Listen-Abstraktionen nicht nützlich sind, wenn Sie mit Iteratoren arbeiten, die einen unendlichen Strom oder eine sehr große Datenmenge zurückgeben. Generator-Ausdrücke sind in diesen Situationen vorzuziehen.
Generator-Ausdrücke werden von Klammern („()“) umschlossen, und Listen-Abstraktionen werden von eckigen Klammern („[]“) umschlossen. Generator-Ausdrücke haben die Form:
( expression for expr in sequence1
if condition1
for expr2 in sequence2
if condition2
for expr3 in sequence3
...
if condition3
for exprN in sequenceN
if conditionN )
Auch hier sind bei einer Listen-Abstraktion nur die äußeren Klammern unterschiedlich (eckige Klammern anstelle von runden Klammern).
Die Elemente der generierten Ausgabe sind die aufeinanderfolgenden Werte von expression. Die if-Klauseln sind alle optional; wenn vorhanden, wird expression nur ausgewertet und zum Ergebnis hinzugefügt, wenn condition wahr ist.
Generator-Ausdrücke müssen immer in runden Klammern geschrieben werden, aber auch die Klammern, die einen Funktionsaufruf signalisieren, zählen. Wenn Sie einen Iterator erstellen möchten, der sofort an eine Funktion übergeben wird, können Sie schreiben:
obj_total = sum(obj.count for obj in list_all_objects())
Die for...in-Klauseln enthalten die zu iterierenden Sequenzen. Die Sequenzen müssen nicht die gleiche Länge haben, da sie von links nach rechts **nicht** parallel iteriert werden. Für jedes Element in sequence1 wird sequence2 von vorne durchlaufen. sequence3 wird dann für jedes resultierende Elementenpaar aus sequence1 und sequence2 durchlaufen.
Anders ausgedrückt, eine Listen-Abstraktion oder ein Generator-Ausdruck ist äquivalent zum folgenden Python-Code:
for expr1 in sequence1:
if not (condition1):
continue # Skip this element
for expr2 in sequence2:
if not (condition2):
continue # Skip this element
...
for exprN in sequenceN:
if not (conditionN):
continue # Skip this element
# Output the value of
# the expression.
Das bedeutet, dass bei mehreren for...in-Klauseln, aber ohne if-Klauseln, die Länge der resultierenden Ausgabe gleich dem Produkt der Längen aller Sequenzen ist. Wenn Sie zwei Listen der Länge 3 haben, ist die Ausgabeliste 9 Elemente lang.
>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3),
('b', 1), ('b', 2), ('b', 3),
('c', 1), ('c', 2), ('c', 3)]
Um eine Mehrdeutigkeit in Pythons Grammatik zu vermeiden, muss expression, wenn es ein Tupel erstellt, von Klammern umschlossen sein. Die erste Listen-Abstraktion unten ist ein Syntaxfehler, während die zweite korrekt ist.
# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]
Generatoren¶
Generatoren sind eine spezielle Klasse von Funktionen, die die Aufgabe des Schreibens von Iteratoren vereinfachen. Reguläre Funktionen berechnen einen Wert und geben ihn zurück, aber Generatoren geben einen Iterator zurück, der einen Strom von Werten zurückgibt.
Sie sind zweifellos vertraut damit, wie reguläre Funktionsaufrufe in Python oder C funktionieren. Wenn Sie eine Funktion aufrufen, erhält sie einen privaten Namensraum, in dem ihre lokalen Variablen erstellt werden. Wenn die Funktion eine return-Anweisung erreicht, werden die lokalen Variablen zerstört und der Wert wird an den Aufrufer zurückgegeben. Ein späterer Aufruf derselben Funktion erstellt einen neuen privaten Namensraum und einen frischen Satz lokaler Variablen. Aber was wäre, wenn die lokalen Variablen beim Verlassen einer Funktion nicht zerstört würden? Was wäre, wenn Sie die Funktion später dort fortsetzen könnten, wo Sie aufgehört haben? Das ist es, was Generatoren bieten; sie können als fortsetzbare Funktionen betrachtet werden.
Hier ist das einfachste Beispiel einer Generatorfunktion
>>> def generate_ints(N):
... for i in range(N):
... yield i
Jede Funktion, die ein yield-Schlüsselwort enthält, ist eine Generatorfunktion. Dies wird vom **Bytecode**-Compiler von Python erkannt, der die Funktion entsprechend speziell kompiliert.
Wenn Sie eine Generatorfunktion aufrufen, gibt sie keinen einzelnen Wert zurück. Stattdessen gibt sie ein Generatorobjekt zurück, das das Iterator-Protokoll unterstützt. Bei der Ausführung des yield-Ausdrucks gibt der Generator den Wert von i aus, ähnlich wie eine return-Anweisung. Der große Unterschied zwischen yield und einer return-Anweisung ist, dass bei Erreichen eines yield der Ausführungszustand des Generators unterbrochen und die lokalen Variablen beibehalten werden. Beim nächsten Aufruf der __next__()-Methode des Generators wird die Funktion weiter ausgeführt.
Hier ist eine Beispielverwendung des Generators generate_ints():
>>> gen = generate_ints(3)
>>> gen
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
File "stdin", line 1, in <module>
File "stdin", line 2, in generate_ints
StopIteration
Sie könnten ebenso for i in generate_ints(5) schreiben, oder a, b, c = generate_ints(3).
Innerhalb einer Generatorfunktion bewirkt return value, dass StopIteration(value) aus der Methode __next__() ausgelöst wird. Sobald dies geschieht oder das Ende der Funktion erreicht ist, endet die Sequenz von Werten, und der Generator kann keine weiteren Werte mehr liefern.
Sie könnten den Effekt von Generatoren manuell erzielen, indem Sie Ihre eigene Klasse schreiben und alle lokalen Variablen des Generators als Instanzvariablen speichern. Zum Beispiel könnte die Rückgabe einer Liste von ganzen Zahlen erfolgen, indem self.count auf 0 gesetzt wird und die Methode __next__() self.count inkrementiert und zurückgibt. Für einen mäßig komplexen Generator kann das Schreiben einer entsprechenden Klasse jedoch viel unübersichtlicher sein.
Die mit Pythons Bibliothek gelieferte Testsuite, Lib/test/test_generators.py, enthält eine Reihe interessanterer Beispiele. Hier ist ein Generator, der eine In-order-Traversal eines Baumes unter Verwendung von Generatoren rekursiv implementiert.
# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
if t:
for x in inorder(t.left):
yield x
yield t.label
for x in inorder(t.right):
yield x
Zwei weitere Beispiele in test_generators.py liefern Lösungen für das N-Damen-Problem (Platzieren von N Damen auf einem NxN-Schachbrett, so dass keine Dame eine andere bedroht) und den Springer-Tour (Finden einer Route, die einen Springer zu jedem Feld eines NxN-Schachbretts führt, ohne ein Feld zweimal zu besuchen).
Werte in einen Generator übergeben¶
In Python 2.4 und früheren Versionen produzierten Generatoren nur Ausgaben. Sobald der Code eines Generators aufgerufen wurde, um einen Iterator zu erstellen, gab es keine Möglichkeit, neue Informationen in die Funktion zu übergeben, wenn ihre Ausführung fortgesetzt wurde. Sie könnten diese Fähigkeit durch den Trick, dass der Generator eine globale Variable betrachtet, oder durch die Übergabe eines veränderlichen Objekts, das Aufrufer dann modifizieren, zusammenbasteln, aber diese Ansätze sind unübersichtlich.
In Python 2.5 gibt es eine einfache Möglichkeit, Werte in einen Generator zu übergeben. yield wurde zu einem Ausdruck, der einen Wert zurückgibt, der einer Variablen zugewiesen oder anderweitig verarbeitet werden kann.
val = (yield i)
Ich empfehle, immer Klammern um einen yield-Ausdruck zu setzen, wenn Sie etwas mit dem zurückgegebenen Wert tun, wie im obigen Beispiel. Die Klammern sind nicht immer notwendig, aber es ist einfacher, sie immer hinzuzufügen, anstatt sich daran zu erinnern, wann sie benötigt werden.
(PEP 342 erklärt die genauen Regeln, nämlich dass ein yield-Ausdruck immer in Klammern stehen muss, es sei denn, er tritt als oberster Ausdruck auf der rechten Seite einer Zuweisung auf. Das bedeutet, Sie können val = yield i schreiben, müssen aber Klammern verwenden, wenn eine Operation stattfindet, wie in val = (yield i) + 12.)
Werte werden in einen Generator gesendet, indem seine Methode send(value) aufgerufen wird. Diese Methode setzt die Ausführung des Generators fort, und der yield-Ausdruck gibt den angegebenen Wert zurück. Wenn die reguläre Methode __next__() aufgerufen wird, gibt yield None zurück.
Hier ist ein einfacher Zähler, der um 1 inkrementiert und das Ändern des internen Zählers ermöglicht.
def counter(maximum):
i = 0
while i < maximum:
val = (yield i)
# If value provided, change counter
if val is not None:
i = val
else:
i += 1
Und hier ist ein Beispiel für das Ändern des Zählers:
>>> it = counter(10)
>>> next(it)
0
>>> next(it)
1
>>> it.send(8)
8
>>> next(it)
9
>>> next(it)
Traceback (most recent call last):
File "t.py", line 15, in <module>
it.next()
StopIteration
Da yield oft None zurückgibt, sollten Sie diesen Fall immer prüfen. Verwenden Sie seinen Wert nicht einfach in Ausdrücken, es sei denn, Sie sind sicher, dass die Methode send() die einzige Methode ist, die zum Wiederaufnehmen Ihrer Generatorfunktion verwendet wird.
Zusätzlich zu send() gibt es zwei weitere Methoden für Generatoren:
throw(value)wird verwendet, um eine Ausnahme innerhalb des Generators auszulösen. Die Ausnahme wird vomyield-Ausdruck ausgelöst, an dem die Ausführung des Generators pausiert ist.close()sendet eineGeneratorExit-Ausnahme an den Generator, um die Iteration zu beenden. Bei Empfang dieser Ausnahme muss der Code des Generators entwederGeneratorExitoderStopIterationauslösen. Das Abfangen der Ausnahme und das Ausführen anderer Aktionen ist illegal und löst eineRuntimeErroraus.close()wird auch vom Garbage Collector von Python aufgerufen, wenn der Generator gesammelt wird.Wenn Sie Bereinigungscode ausführen müssen, wenn ein
GeneratorExitauftritt, schlage ich vor, einetry: ... finally:-Suite zu verwenden, anstattGeneratorExitabzufangen.
Der kumulative Effekt dieser Änderungen besteht darin, Generatoren von einseitigen Informationsproduzenten zu Produzenten und Konsumenten zu machen.
Generatoren werden auch zu **Koroutinen**, einer verallgemeinerten Form von Subroutinen. Subroutinen werden an einem Punkt aufgerufen und an einem anderen Punkt verlassen (die Spitze der Funktion und eine return-Anweisung), aber Koroutinen können an vielen verschiedenen Punkten aufgerufen, verlassen und fortgesetzt werden (die yield-Anweisungen).
Eingebaute Funktionen¶
Betrachten wir genauer die eingebauten Funktionen, die häufig mit Iteratoren verwendet werden.
Zwei von Pythons eingebauten Funktionen, map() und filter(), duplizieren die Funktionen von Generator-Ausdrücken.
map(f, iterA, iterB, ...)gibt einen Iterator über die Sequenz zurück:f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ....>>> def upper(s): ... return s.upper()
>>> list(map(upper, ['sentence', 'fragment'])) ['SENTENCE', 'FRAGMENT'] >>> [upper(s) for s in ['sentence', 'fragment']] ['SENTENCE', 'FRAGMENT']
Sie können denselben Effekt natürlich auch mit einer Listen-Abstraktion erzielen.
filter(predicate, iter) gibt einen Iterator über alle Elemente der Sequenz zurück, die eine bestimmte Bedingung erfüllen, und wird ähnlich durch Listen-Abstraktionen dupliziert. Ein Prädikat ist eine Funktion, die den Wahrheitswert einer Bedingung zurückgibt; für die Verwendung mit filter() muss das Prädikat einen einzelnen Wert annehmen.
>>> def is_even(x):
... return (x % 2) == 0
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]
Dies kann auch als Listen-Abstraktion geschrieben werden
>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]
enumerate(iter, start=0) zählt die Elemente in der iterierbaren Sammlung ab und gibt Tupel der Länge 2 zurück, die die Zählung (ab start) und jedes Element enthalten.
>>> for item in enumerate(['subject', 'verb', 'object']):
... print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')
enumerate() wird häufig verwendet, wenn eine Liste durchlaufen wird und die Indizes aufgezeichnet werden, bei denen bestimmte Bedingungen erfüllt sind
f = open('data.txt', 'r')
for i, line in enumerate(f):
if line.strip() == '':
print('Blank line at line #%i' % i)
sorted(iterable, key=None, reverse=False) sammelt alle Elemente des iterierbaren Objekts in einer Liste, sortiert die Liste und gibt das sortierte Ergebnis zurück. Die Argumente key und reverse werden an die sort()-Methode der erstellten Liste weitergegeben.
>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]
(Für eine detailliertere Diskussion über das Sortieren siehe die Sortiertechniken.)
Die integrierten Funktionen any(iter) und all(iter) betrachten die Wahrheitswerte der Inhalte eines iterierbaren Objekts. any() gibt True zurück, wenn irgendein Element im iterierbaren Objekt einen wahren Wert hat, und all() gibt True zurück, wenn alle Elemente wahre Werte sind
>>> any([0, 1, 0])
True
>>> any([0, 0, 0])
False
>>> any([1, 1, 1])
True
>>> all([0, 1, 0])
False
>>> all([0, 0, 0])
False
>>> all([1, 1, 1])
True
zip(iterA, iterB, ...) nimmt ein Element aus jedem iterierbaren Objekt und gibt sie in einem Tupel zurück
zip(['a', 'b', 'c'], (1, 2, 3)) =>
('a', 1), ('b', 2), ('c', 3)
Es wird keine Liste im Speicher erstellt und alle Eingabe-Iteratoren erschöpft, bevor etwas zurückgegeben wird; stattdessen werden Tupel erstellt und nur zurückgegeben, wenn sie angefordert werden. (Der technische Begriff für dieses Verhalten ist lazy evaluation.)
Dieser Iterator ist dafür gedacht, mit iterierbaren Objekten gleicher Länge verwendet zu werden. Wenn die iterierbaren Objekte unterschiedliche Längen haben, wird der resultierende Strom die gleiche Länge wie das kürzeste iterierbare Objekt haben.
zip(['a', 'b'], (1, 2, 3)) =>
('a', 1), ('b', 2)
Sie sollten dies jedoch vermeiden, da ein Element von den längeren Iteratoren entnommen und verworfen werden kann. Das bedeutet, dass Sie die Iteratoren nicht weiter verwenden können, da Sie Gefahr laufen, ein verworfenes Element zu überspringen.
Das itertools-Modul¶
Das itertools-Modul enthält eine Reihe von gängigen Iteratoren sowie Funktionen zur Kombination mehrerer Iteratoren. Dieser Abschnitt stellt den Inhalt des Moduls anhand kleiner Beispiele vor.
Die Funktionen des Moduls lassen sich in einige breite Klassen einteilen
Funktionen, die einen neuen Iterator basierend auf einem bestehenden Iterator erstellen.
Funktionen zur Behandlung der Elemente eines Iterators als Funktionsargumente.
Funktionen zur Auswahl von Teilen der Ausgabe eines Iterators.
Eine Funktion zum Gruppieren der Ausgabe eines Iterators.
Erstellen neuer Iteratoren¶
itertools.count(start, step) gibt einen unendlichen Strom gleichmäßig beabstandeter Werte zurück. Sie können optional die Startzahl angeben, die standardmäßig 0 ist, und den Abstand zwischen den Zahlen, der standardmäßig 1 ist
itertools.count() =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.count(10, 5) =>
10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...
itertools.cycle(iter) speichert eine Kopie des Inhalts eines bereitgestellten iterierbaren Objekts und gibt einen neuen Iterator zurück, der dessen Elemente von vorne bis hinten zurückgibt. Der neue Iterator wiederholt diese Elemente unendlich oft.
itertools.cycle([1, 2, 3, 4, 5]) =>
1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
itertools.repeat(elem, [n]) gibt das bereitgestellte Element n Mal zurück, oder gibt das Element endlos zurück, wenn n nicht angegeben ist.
itertools.repeat('abc') =>
abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
abc, abc, abc, abc, abc
itertools.chain(iterA, iterB, ...) nimmt eine beliebige Anzahl von iterierbaren Objekten als Eingabe und gibt alle Elemente des ersten Iterators zurück, dann alle Elemente des zweiten und so weiter, bis alle iterierbaren Objekte erschöpft sind.
itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
a, b, c, 1, 2, 3
itertools.islice(iter, [start], stop, [step]) gibt einen Strom zurück, der ein Slice des Iterators ist. Mit einem einzigen stop-Argument gibt er die ersten stop-Elemente zurück. Wenn Sie einen Startindex angeben, erhalten Sie stop-start-Elemente, und wenn Sie einen Wert für step angeben, werden die Elemente entsprechend übersprungen. Im Gegensatz zu Python-Slicing für Zeichenketten und Listen können Sie keine negativen Werte für start, stop oder step verwenden.
itertools.islice(range(10), 8) =>
0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
2, 4, 6
itertools.tee(iter, [n]) repliziert einen Iterator; er gibt n unabhängige Iteratoren zurück, die alle den Inhalt des Quelliterators zurückgeben. Wenn Sie keinen Wert für n angeben, ist der Standardwert 2. Das Replizieren von Iteratoren erfordert das Speichern eines Teils des Inhalts des Quelliterators, so dass dies erheblichen Speicher verbrauchen kann, wenn der Iterator groß ist und einer der neuen Iteratoren mehr als die anderen verbraucht wird.
itertools.tee( itertools.count() ) =>
iterA, iterB
where iterA ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
and iterB ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
Funktionen auf Elemente anwenden¶
Das operator-Modul enthält eine Reihe von Funktionen, die den Operatoren von Python entsprechen. Einige Beispiele sind operator.add(a, b) (addiert zwei Werte), operator.ne(a, b) (entspricht a != b) und operator.attrgetter('id') (gibt ein aufrufbares Objekt zurück, das das Attribut .id abruft).
itertools.starmap(func, iter) geht davon aus, dass das iterierbare Objekt einen Strom von Tupeln zurückgibt, und ruft func mit diesen Tupeln als Argumente auf
itertools.starmap(os.path.join,
[('/bin', 'python'), ('/usr', 'bin', 'java'),
('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
/bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby
Elemente auswählen¶
Eine weitere Gruppe von Funktionen wählt eine Teilmenge der Elemente eines Iterators basierend auf einem Prädikat aus.
itertools.filterfalse(predicate, iter) ist das Gegenteil von filter() und gibt alle Elemente zurück, für die das Prädikat falsch ist
itertools.filterfalse(is_even, itertools.count()) =>
1, 3, 5, 7, 9, 11, 13, 15, ...
itertools.takewhile(predicate, iter) gibt Elemente zurück, solange das Prädikat wahr ist. Sobald das Prädikat falsch zurückgibt, signalisiert der Iterator das Ende seiner Ergebnisse.
def less_than_10(x):
return x < 10
itertools.takewhile(less_than_10, itertools.count()) =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
itertools.takewhile(is_even, itertools.count()) =>
0
itertools.dropwhile(predicate, iter) verwirft Elemente, solange das Prädikat wahr ist, und gibt dann den Rest der Ergebnisse des iterierbaren Objekts zurück.
itertools.dropwhile(less_than_10, itertools.count()) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.dropwhile(is_even, itertools.count()) =>
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...
itertools.compress(data, selectors) nimmt zwei Iteratoren und gibt nur die Elemente von data zurück, für die das entsprechende Element von selectors wahr ist, und stoppt, sobald einer der beiden erschöpft ist
itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
1, 2, 5
Kombinatorische Funktionen¶
Das itertools.combinations(iterable, r) gibt einen Iterator zurück, der alle möglichen r-Tupel-Kombinationen der Elemente im iterable liefert.
itertools.combinations([1, 2, 3, 4, 5], 2) =>
(1, 2), (1, 3), (1, 4), (1, 5),
(2, 3), (2, 4), (2, 5),
(3, 4), (3, 5),
(4, 5)
itertools.combinations([1, 2, 3, 4, 5], 3) =>
(1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
(2, 3, 4), (2, 3, 5), (2, 4, 5),
(3, 4, 5)
Die Elemente innerhalb jedes Tupels bleiben in der gleichen Reihenfolge, in der sie von iterable zurückgegeben wurden. Zum Beispiel ist die Zahl 1 in den obigen Beispielen immer vor 2, 3, 4 oder 5. Eine ähnliche Funktion, itertools.permutations(iterable, r=None), hebt diese Einschränkung auf die Reihenfolge auf und gibt alle möglichen Anordnungen der Länge r zurück
itertools.permutations([1, 2, 3, 4, 5], 2) =>
(1, 2), (1, 3), (1, 4), (1, 5),
(2, 1), (2, 3), (2, 4), (2, 5),
(3, 1), (3, 2), (3, 4), (3, 5),
(4, 1), (4, 2), (4, 3), (4, 5),
(5, 1), (5, 2), (5, 3), (5, 4)
itertools.permutations([1, 2, 3, 4, 5]) =>
(1, 2, 3, 4, 5), (1, 2, 3, 5, 4), (1, 2, 4, 3, 5),
...
(5, 4, 3, 2, 1)
Wenn Sie keinen Wert für r angeben, wird die Länge des iterierbaren Objekts verwendet, was bedeutet, dass alle Elemente permutiert werden.
Beachten Sie, dass diese Funktionen alle möglichen Kombinationen nach Position erzeugen und nicht erfordern, dass die Inhalte von iterable eindeutig sind
itertools.permutations('aba', 3) =>
('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')
Das identische Tupel ('a', 'a', 'b') kommt zweimal vor, aber die beiden 'a'-Strings stammen aus unterschiedlichen Positionen.
Die Funktion itertools.combinations_with_replacement(iterable, r) lockert eine andere Einschränkung: Elemente können innerhalb eines Tupels wiederholt werden. Konzeptionell wird ein Element für die erste Position jedes Tupels ausgewählt und dann ersetzt, bevor das zweite Element ausgewählt wird.
itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2) =>
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
(2, 2), (2, 3), (2, 4), (2, 5),
(3, 3), (3, 4), (3, 5),
(4, 4), (4, 5),
(5, 5)
Elemente gruppieren¶
Die letzte Funktion, die ich besprechen werde, itertools.groupby(iter, key_func=None), ist die komplizierteste. key_func(elem) ist eine Funktion, die einen Schlüsselwert für jedes vom Iterator zurückgegebene Element berechnen kann. Wenn Sie keine Schlüsselfunktion angeben, ist der Schlüssel einfach jedes Element selbst.
groupby() sammelt alle aufeinanderfolgenden Elemente aus dem zugrundeliegenden iterierbaren Objekt mit demselben Schlüsselwert und gibt einen Strom von 2-Tupeln zurück, die einen Schlüsselwert und einen Iterator für die Elemente mit diesem Schlüssel enthalten.
city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
('Anchorage', 'AK'), ('Nome', 'AK'),
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
...
]
def get_state(city_state):
return city_state[1]
itertools.groupby(city_list, get_state) =>
('AL', iterator-1),
('AK', iterator-2),
('AZ', iterator-3), ...
where
iterator-1 =>
('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')
groupby() geht davon aus, dass die Inhalte des zugrundeliegenden iterierbaren Objekts bereits nach dem Schlüssel sortiert sind. Beachten Sie, dass die zurückgegebenen Iteratoren auch das zugrundeliegende iterierbare Objekt verwenden. Sie müssen also die Ergebnisse von Iterator-1 verbrauchen, bevor Sie Iterator-2 und seinen entsprechenden Schlüssel anfordern.
Das functools-Modul¶
Das functools-Modul enthält einige höherstufige Funktionen. Eine höherstufige Funktion nimmt eine oder mehrere Funktionen als Eingabe und gibt eine neue Funktion zurück. Das nützlichste Werkzeug in diesem Modul ist die Funktion functools.partial().
Für Programme, die in einem funktionalen Stil geschrieben sind, möchten Sie manchmal Varianten bestehender Funktionen erstellen, bei denen einige Parameter bereits ausgefüllt sind. Betrachten Sie eine Python-Funktion f(a, b, c); Sie möchten möglicherweise eine neue Funktion g(b, c) erstellen, die äquivalent zu f(1, b, c) ist; Sie füllen einen Wert für einen der Parameter von f() aus. Dies wird als „Partialfunktionsanwendung“ bezeichnet.
Der Konstruktor für partial() nimmt die Argumente (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2). Das resultierende Objekt ist aufrufbar, sodass Sie es einfach aufrufen können, um function mit den ausgefüllten Argumenten aufzurufen.
Hier ist ein kleines, aber realistisches Beispiel
import functools
def log(message, subsystem):
"""Write the contents of 'message' to the specified subsystem."""
print('%s: %s' % (subsystem, message))
...
server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')
functools.reduce(func, iter, [initial_value]) führt kumulativ eine Operation auf allen Elementen des iterierbaren Objekts aus und kann daher nicht auf unendliche Iteratoren angewendet werden. func muss eine Funktion sein, die zwei Elemente aufnimmt und einen einzelnen Wert zurückgibt. functools.reduce() nimmt die ersten beiden Elemente A und B, die vom Iterator zurückgegeben werden, und berechnet func(A, B). Dann fordert es das dritte Element, C, an, berechnet func(func(A, B), C), kombiniert dieses Ergebnis mit dem vierten zurückgegebenen Element und fährt fort, bis das iterierbare Objekt erschöpft ist. Wenn das iterierbare Objekt überhaupt keine Werte zurückgibt, wird eine TypeError-Ausnahme ausgelöst. Wenn der Startwert angegeben ist, wird er als Ausgangspunkt verwendet und func(initial_value, A) ist die erste Berechnung.
>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
...
TypeError: reduce() of empty sequence with no initial value
>>> functools.reduce(operator.mul, [1, 2, 3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1
Wenn Sie operator.add() mit functools.reduce() verwenden, addieren Sie alle Elemente des iterierbaren Objekts. Dieser Fall ist so häufig, dass es eine spezielle integrierte Funktion namens sum() gibt, um ihn zu berechnen
>>> import functools, operator
>>> functools.reduce(operator.add, [1, 2, 3, 4], 0)
10
>>> sum([1, 2, 3, 4])
10
>>> sum([])
0
Für viele Verwendungen von functools.reduce() ist es jedoch klarer, einfach die offensichtliche for-Schleife zu schreiben
import functools
# Instead of:
product = functools.reduce(operator.mul, [1, 2, 3], 1)
# You can write:
product = 1
for i in [1, 2, 3]:
product *= i
Eine verwandte Funktion ist itertools.accumulate(iterable, func=operator.add). Sie führt die gleiche Berechnung durch, gibt aber anstelle des endgültigen Ergebnisses nur ein Iterator zurück, der auch jedes Teilergebnis liefert
itertools.accumulate([1, 2, 3, 4, 5]) =>
1, 3, 6, 10, 15
itertools.accumulate([1, 2, 3, 4, 5], operator.mul) =>
1, 2, 6, 24, 120
Das operator-Modul¶
Das operator-Modul wurde bereits erwähnt. Es enthält eine Reihe von Funktionen, die den Operatoren von Python entsprechen. Diese Funktionen sind oft im funktionalen Stil nützlich, da sie verhindern, dass Sie triviale Funktionen schreiben, die eine einzelne Operation ausführen.
Einige der Funktionen in diesem Modul sind
Mathematische Operationen:
add(),sub(),mul(),floordiv(),abs(), …Logische Operationen:
not_(),truth().Bitweise Operationen:
and_(),or_(),invert().Vergleiche:
eq(),ne(),lt(),le(),gt()undge().Objektidentität:
is_(),is_not().
Konsultieren Sie die Dokumentation des operator-Moduls für eine vollständige Liste.
Kleine Funktionen und die Lambda-Ausdrücke¶
Beim Schreiben von Programmen im funktionalen Stil benötigen Sie oft kleine Funktionen, die als Prädikate dienen oder Elemente auf irgendeine Weise kombinieren.
Wenn eine Python-integrierte Funktion oder eine Modulfunktion geeignet ist, müssen Sie keine neue Funktion definieren
stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)
Wenn die benötigte Funktion nicht existiert, müssen Sie sie schreiben. Ein Weg, kleine Funktionen zu schreiben, ist die Verwendung des lambda-Ausdrucks. lambda nimmt eine Anzahl von Parametern und einen Ausdruck, der diese Parameter kombiniert, und erstellt eine anonyme Funktion, die den Wert des Ausdrucks zurückgibt
adder = lambda x, y: x+y
print_assign = lambda name, value: name + '=' + str(value)
Eine Alternative ist, einfach die def-Anweisung zu verwenden und eine Funktion wie üblich zu definieren
def adder(x, y):
return x + y
def print_assign(name, value):
return name + '=' + str(value)
Welche Alternative ist vorzuziehen? Das ist eine Stilfrage; mein üblicher Weg ist es, lambda zu vermeiden.
Ein Grund für meine Präferenz ist, dass lambda in den Funktionen, die es definieren kann, ziemlich begrenzt ist. Das Ergebnis muss als einzelner Ausdruck berechenbar sein, was bedeutet, dass Sie keine mehrstufigen if... elif... else-Vergleiche oder try... except-Anweisungen haben können. Wenn Sie versuchen, zu viel in einer lambda-Anweisung zu tun, erhalten Sie einen übermäßig komplizierten Ausdruck, der schwer zu lesen ist. Schnell, was macht der folgende Code?
import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]
Sie können es herausfinden, aber es dauert Zeit, den Ausdruck zu entwirren, um zu verstehen, was vor sich geht. Die Verwendung kurzer, verschachtelter def-Anweisungen macht die Sache etwas besser
import functools
def combine(a, b):
return 0, a[1] + b[1]
total = functools.reduce(combine, items)[1]
Aber es wäre am besten, wenn ich einfach eine for-Schleife verwendet hätte
total = 0
for a, b in items:
total += b
Oder die integrierte Funktion sum() und ein Generator-Ausdruck
total = sum(b for a, b in items)
Viele Verwendungen von functools.reduce() sind klarer, wenn sie als for-Schleifen geschrieben sind.
Fredrik Lundh schlug einst die folgenden Regeln für das Refactoring von lambda-Verwendungen vor
Schreiben Sie eine Lambda-Funktion.
Schreiben Sie einen Kommentar, der erklärt, was diese Lambda-Funktion tut.
Studieren Sie den Kommentar eine Weile und überlegen Sie sich einen Namen, der die Essenz des Kommentars erfasst.
Konvertieren Sie die Lambda-Funktion in eine `def`-Anweisung mit diesem Namen.
Entfernen Sie den Kommentar.
Ich mag diese Regeln sehr, aber Sie können anderer Meinung sein, ob dieser Lambda-freie Stil besser ist.
Revisionsverlauf und Danksagungen¶
Der Autor dankt den folgenden Personen für Vorschläge, Korrekturen und Unterstützung bei verschiedenen Entwürfen dieses Artikels: Ian Bicking, Nick Coghlan, Nick Efford, Raymond Hettinger, Jim Jewett, Mike Krell, Leandro Lameiro, Jussi Salmela, Collin Winter, Blake Winton.
Version 0.1: Veröffentlicht am 30. Juni 2006.
Version 0.11: Veröffentlicht am 1. Juli 2006. Tippfehlerkorrekturen.
Version 0.2: Veröffentlicht am 10. Juli 2006. `genexp` und `listcomp` Abschnitte zu einem zusammengeführt. Tippfehlerkorrekturen.
Version 0.21: Weitere Referenzen hinzugefügt, die auf der Tutor-Mailingliste vorgeschlagen wurden.
Version 0.30: Fügt einen Abschnitt über das von Collin Winter geschriebene functional-Modul hinzu; fügt einen kurzen Abschnitt über das operator-Modul hinzu; einige andere Bearbeitungen.
Referenzen¶
Allgemein¶
Structure and Interpretation of Computer Programs, von Harold Abelson und Gerald Jay Sussman mit Julie Sussman. Das Buch ist zu finden unter https://mitpress.mit.edu/sicp. In diesem klassischen Lehrbuch der Informatik werden in den Kapiteln 2 und 3 die Verwendung von Sequenzen und Streams zur Organisation des Datenflusses innerhalb eines Programms diskutiert. Das Buch verwendet Scheme für seine Beispiele, aber viele der in diesen Kapiteln beschriebenen Designansätze sind auf Python-Code im funktionalen Stil anwendbar.
https://defmacro.org/2006/06/19/fp.html: Eine allgemeine Einführung in die funktionale Programmierung, die Java-Beispiele verwendet und eine ausführliche historische Einführung enthält.
https://en.wikipedia.org/wiki/Functional_programming: Allgemeiner Wikipedia-Eintrag, der funktionale Programmierung beschreibt.
https://en.wikipedia.org/wiki/Coroutine: Eintrag für Coroutinen.
https://en.wikipedia.org/wiki/Partial_application: Eintrag für das Konzept der Partialfunktionsanwendung.
https://en.wikipedia.org/wiki/Currying: Eintrag für das Konzept des Currying.
Python-spezifisch¶
https://gnosis.cx/TPiP/: Das erste Kapitel von David Mertz' Buch Text Processing in Python befasst sich mit funktionaler Programmierung zur Textverarbeitung, im Abschnitt mit dem Titel „Utilizing Higher-Order Functions in Text Processing“.
Mertz schrieb auch eine 3-teilige Artikelreihe über funktionale Programmierung für die DeveloperWorks-Website von IBM; siehe Teil 1, Teil 2 und Teil 3.
Python-Dokumentation¶
Dokumentation für das itertools-Modul.
Dokumentation für das functools-Modul.
Dokumentation für das operator-Modul.
PEP 289: „Generator Expressions“
PEP 342: „Coroutines via Enhanced Generators“ beschreibt die neuen Generator-Features in Python 2.5.