Anhang A - Pythonic
Anhang A: Was heißt “pythonic”?
Abschnitt betitelt „Anhang A: Was heißt “pythonic”?“In Code-Reviews und in der Python-Gemeinde fällt ständig dasselbe Wort: Eine Lösung sei pythonic oder eben nicht. Gemeint ist damit kein Geschmacksurteil, sondern etwas Konkretes: Pythonic ist Code, der die Mittel der Sprache so einsetzt, wie sie gedacht sind, statt Muster aus anderen Sprachen nachzubauen. Unpythonic ist Code fast nie, weil er falsch wäre; er läuft, liefert das richtige Ergebnis und wäre in einer anderen Sprache völlig normal. Er ist nur länger, fehleranfälliger oder schwerer zu lesen als nötig, weil er gegen die Sprache arbeitet statt mit ihr.
Dieser Anhang sammelt die wichtigsten Muster als Gegenüberstellungen. Er ist als Nachschlagewerk gedacht: Die Beispiele setzen unterschiedlich viel Stoff des Schuljahres voraus, und niemand muss sie am Schuljahresbeginn alle verstehen. Wer eine Übungsabgabe fertig hat, kann sie gegen diese Liste halten; das ist zugleich eine gute Vorbereitung auf die Code-Reviews im Unterricht. Die philosophische Kurzfassung dieses Anhangs steht übrigens in der Sprache selbst, siehe Anhang B: The Zen of Python.
Über Sequenzen iterieren
Abschnitt betitelt „Über Sequenzen iterieren“Der häufigste Fall zuerst. Wer die Elemente einer Liste braucht, iteriert direkt über die Liste, nicht über ihre Indizes:
colors = ["red", "green", "blue"]
# unpythonic: index bookkeeping for no reasonfor i in range(len(colors)): print(colors[i])
# pythonic: iterate over the elements themselvesfor color in colors: print(color)Die zweite Fassung sagt direkt, was gemeint ist (“jede Farbe, der Reihe nach”), und macht Indexfehler unmöglich. Der Index ist nur dann interessant, wenn er wirklich gebraucht wird, und dann liefert enumerate() beides zugleich:
# unpythonic: manual counter next to the loopi = 0for color in colors: print(f"{i}: {color}") i += 1
# pythonicfor i, color in enumerate(colors): print(f"{i}: {color}")Für das parallele Durchlaufen zweier Listen gibt es zip():
names = ["intro", "main", "outro"]durations = [90, 845, 45]
# unpythonicfor i in range(len(names)): print(f"{names[i]}: {durations[i]} s")
# pythonicfor name, duration in zip(names, durations): print(f"{name}: {duration} s")Mehrfachzuweisung und Tauschen
Abschnitt betitelt „Mehrfachzuweisung und Tauschen“Python kann mehrere Werte in einem Schritt zuweisen (tuple unpacking). Das ersetzt Hilfsvariablen, die es nur der Mechanik wegen gibt:
# unpythonic: swap with a temporary variabletemp = aa = bb = temp
# pythonica, b = b, aDasselbe Muster steckt in for key, value in data.items() und in Funktionen, die mehrere Werte zurückgeben (return index, count).
Wahrheitswerte nutzen
Abschnitt betitelt „Wahrheitswerte nutzen“Leere Sammlungen, leere Strings, 0 und None gelten in Python als falsch, alles andere als wahr (truthiness). Explizite Vergleiche mit Länge oder Literalen sind deshalb Umwege:
# unpythonicif len(tasks) > 0: ...if is_valid == True: ...
# pythonicif tasks: ...if is_valid: ...Eine bewusste Ausnahme: Der Vergleich mit None wird ausdrücklich geschrieben, und zwar mit is, weil None ein eindeutiges Einzelobjekt ist: if result is None:.
Eingebaute Funktionen statt Handarbeit
Abschnitt betitelt „Eingebaute Funktionen statt Handarbeit“Summe, Minimum, Maximum, Sortierung und “kommt x vor?” sind eingebaut. Eigene Schleifen dafür sind Übungsmaterial, aber kein Produktionscode:
values = [4, 1, 7, 3]
# unpythonictotal = 0for value in values: total += value
# pythonictotal = sum(values)largest = max(values)ordered = sorted(values)found = 7 in valuesVerallgemeinert: Vor dem Selbstschreiben in der Standardbibliothek nachsehen. Häufigkeiten zählt collections.Counter, Pfade verwaltet pathlib, CSV und JSON haben eigene Module. Das Nachbauen vorhandener Bibliotheksfunktionen ist die teuerste Form von unpythonic, weil sie auch noch die Fehler der eigenen Nachbauten einkauft.
List Comprehensions, mit Augenmaß
Abschnitt betitelt „List Comprehensions, mit Augenmaß“Für das Muster “neue Liste aus alter Liste, Element für Element umgeformt oder gefiltert” gibt es eine eigene Syntax:
# unpythonic for this simple casesquares = []for x in range(1, 21): squares.append(x * x)
# pythonicsquares = [x * x for x in range(1, 21)]jpegs = [p for p in paths if p.suffix == ".jpg"]Augenmaß heißt: Eine Comprehension ist pythonic, solange sie auf einen Blick lesbar bleibt. Verschachtelte Comprehensions mit mehreren Bedingungen sind es nicht mehr; dann ist die ausgeschriebene Schleife die bessere, weil lesbarere Lösung. Kompaktheit ist ein Mittel, kein Ziel.
Strings bauen mit f-Strings
Abschnitt betitelt „Strings bauen mit f-Strings“# unpythonic: concatenation with type conversionsmessage = "frame " + str(frames) + " of " + str(total)
# pythonicmessage = f"frame {frames} of {total}"Dateien mit with öffnen
Abschnitt betitelt „Dateien mit with öffnen“# unpythonic: closing manually, or worse, not at allfile = open("notes.txt", encoding="utf-8")content = file.read()file.close()
# pythonic: closed automatically, even on errorswith open("notes.txt", encoding="utf-8") as file: content = file.read()Der with-Block garantiert das Schließen auch im Fehlerfall (Kapitel 9). Dasselbe gilt überall, wo Ressourcen belegt werden.
EAFP: erst tun, dann behandeln
Abschnitt betitelt „EAFP: erst tun, dann behandeln“Python bevorzugt den Stil “Easier to Ask Forgiveness than Permission”: die Operation versuchen und den Fehlerfall behandeln, statt vorab jede Bedingung zu prüfen (Kapitel 7):
# unpythonic (LBYL: look before you leap)if "duration" in clip and isinstance(clip["duration"], int): seconds = clip["duration"]else: seconds = 0
# pythonic (EAFP)try: seconds = int(clip["duration"])except (KeyError, ValueError): seconds = 0Für den häufigen Spezialfall “Schlüssel mit Ersatzwert” gibt es die noch kürzere Form clip.get("duration", 0).
Kapselung ohne Getter-Wald
Abschnitt betitelt „Kapselung ohne Getter-Wald“Wer aus Sprachen mit getX()/setX()-Konventionen kommt, baut diese gerne nach. Python löst dasselbe Problem mit direktem Attributzugriff und, sobald Kontrolle nötig ist, mit Properties (Kapitel 4):
# unpythonic: Java-style accessorsclass Clip: def get_name(self): return self._name
def set_name(self, name): self._name = name
# pythonic: plain attribute, property only when logic is neededclass Clip: def __init__(self, name): self.name = nameDer Punkt dahinter: Ein Getter ohne Logik ist Zeremonie. Braucht das Attribut später Validierung, wird es zur Property, ohne dass sich für die Aufrufer etwas ändert; genau dafür existiert der Mechanismus.
Konventionen sind Teil der Sprache
Abschnitt betitelt „Konventionen sind Teil der Sprache“Pythonic ist schließlich auch, was PEP 8 festlegt: snake_case für Funktionen und Variablen, PascalCase für Klassen, UPPER_CASE für Konstanten, 4 Leerzeichen Einrückung, Docstrings für Funktionen und Klassen. Code, der diese Konventionen verletzt, wird von jedem Python-Leser als fremd wahrgenommen, unabhängig davon, wie gut er funktioniert.
Checkliste für die eigene Abgabe
Abschnitt betitelt „Checkliste für die eigene Abgabe“- Iteriere ich irgendwo mit
range(len(...)), obwohl ich die Elemente will? - Zähle, summiere oder suche ich von Hand, was eine eingebaute Funktion kann?
- Vergleiche ich mit
== Trueoderlen(...) > 0? - Öffne ich Dateien ohne
with? - Baue ich Strings mit
+undstr()zusammen? - Habe ich Getter und Setter ohne jede Logik geschrieben?
- Halte ich die PEP-8-Namenskonventionen durch?