Zum Inhalt springen

Anhang A - Pythonic

Zu Zen-Modus wechseln

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.

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 reason
for i in range(len(colors)):
print(colors[i])
# pythonic: iterate over the elements themselves
for 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 loop
i = 0
for color in colors:
print(f"{i}: {color}")
i += 1
# pythonic
for 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]
# unpythonic
for i in range(len(names)):
print(f"{names[i]}: {durations[i]} s")
# pythonic
for name, duration in zip(names, durations):
print(f"{name}: {duration} s")

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 variable
temp = a
a = b
b = temp
# pythonic
a, b = b, a

Dasselbe Muster steckt in for key, value in data.items() und in Funktionen, die mehrere Werte zurückgeben (return index, count).

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:

# unpythonic
if len(tasks) > 0:
...
if is_valid == True:
...
# pythonic
if 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:.

Summe, Minimum, Maximum, Sortierung und “kommt x vor?” sind eingebaut. Eigene Schleifen dafür sind Übungsmaterial, aber kein Produktionscode:

values = [4, 1, 7, 3]
# unpythonic
total = 0
for value in values:
total += value
# pythonic
total = sum(values)
largest = max(values)
ordered = sorted(values)
found = 7 in values

Verallgemeinert: 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.

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 case
squares = []
for x in range(1, 21):
squares.append(x * x)
# pythonic
squares = [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.

# unpythonic: concatenation with type conversions
message = "frame " + str(frames) + " of " + str(total)
# pythonic
message = f"frame {frames} of {total}"
# unpythonic: closing manually, or worse, not at all
file = open("notes.txt", encoding="utf-8")
content = file.read()
file.close()
# pythonic: closed automatically, even on errors
with 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.

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 = 0

Für den häufigen Spezialfall “Schlüssel mit Ersatzwert” gibt es die noch kürzere Form clip.get("duration", 0).

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 accessors
class Clip:
def get_name(self):
return self._name
def set_name(self, name):
self._name = name
# pythonic: plain attribute, property only when logic is needed
class Clip:
def __init__(self, name):
self.name = name

Der 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.

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.

  • 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 == True oder len(...) > 0?
  • Öffne ich Dateien ohne with?
  • Baue ich Strings mit + und str() zusammen?
  • Habe ich Getter und Setter ohne jede Logik geschrieben?
  • Halte ich die PEP-8-Namenskonventionen durch?