Worum es geht. Verzweigen, wiederholen, in Funktionen kapseln — und dazu die vier Konstrukte, für die es in Java keine direkte Entsprechung gibt: strukturelles Pattern Matching, Comprehensions, das Iteratorprotokoll und Dekoratoren.
Was Sie danach können. Ein match mit Struktur-Pattern lesen und die Bindungs-Falle vermeiden; eine Comprehension von einer Generator Expression unterscheiden und wissen, wann welche; einen eigenen Iterator und einen eigenen Dekorator schreiben; begründen, warum global ein Warnsignal ist; und einen eigenen with-Block bauen.
Die Java-Entsprechung in einem Satz. match entspricht dem Pattern Matching für switch aus Java 21 und eine Generator Expression einem faulen Stream — ein Dekorator ist dagegen gerade keine Annotation, sondern ausführbarer Code, der das dekorierte Objekt austauscht.
if then else¶
Anstelle von then verwendet man einen einfachen Doppelpunkt, anstelle von Klammern um einen Codeblock abzugrenzen verwendet man Einrückungen (hier: 4 Leerzeichen):
if 42 > 23:
x = 42 - 23
else:
x = 0
print(x)
# Output: 19
Dabei sollte auffallen, dass der then-Block keinen eigenen Scope hat, was wir daran merken dass wir auf die Variable x auch außerhalb, nach dem if-then-else Zugriff haben (auch wenn x nie zuvor deklariert wurde). Auf den else-Block kann man verzichten.
Pythonic (also idiomatisch für die Sprache Python) ist z.B.
x = "Ein möglicherweise leerer String"
if x:
print("Super!")
# Output: Super!
x = ""
if not x:
print("Auch gut")
# Output: Auch gut
ternärer Operator¶
Wenn man Variablen über eine Fallunterscheidung zuweist, kann man das syntaktisch etwas kürzer machen:
x = "super" if 42 > 23 else "nicht so gut"
print(x)
# Output: super
elif und match case¶
Mehrere Alternativen lassen sich durch eine Sequenz von else-if-Abfragen schreiben; um hier eine verschachtelte Hierarchie und tiefe Einrückungen zu vermeiden, gibt es das elif-Keyword:
x = "Eingabe"
if "c" in x:
print("Nein")
elif "d" in x:
print("Auch nicht")
elif "e" in x:
print("Ja, genau!")
elif "f" in x:
print("Nee")
else:
print("Sonstiges")
# Output: Ja, genau!
Eine modernere Konstruktion seit Python 3.10 ist Python’s Variante von switch-case:
http_status = 404
match http_status:
case 200:
print("OK")
case 403 | 404:
print("Forbidden or Not Found")
case 500:
print("Server Error")
case status_code if status_code > 500:
print("Higher error", status_code)
case _:
print("Other status code")
# Output: Forbidden or Not Found
Allerdings ist match sehr viel mächtiger als eine Kette von elifs, denn man kann damit strukturelles Pattern-Matching betreiben und eine ganze Menge von Fällen gemeinsam behandeln.
Strukturelles Pattern Matching¶
Ein case vergleicht nicht nur Werte, es zerlegt den Gegenstand: Sequenzen, Dictionaries, Klassen und Verschachtelungen davon. Die dabei gebundenen Namen stehen im zugehörigen Block zur Verfügung.
from dataclasses import dataclass
@dataclass
class Punkt:
x: int
y: int
def beschreibe(ding):
match ding:
case (0, 0):
return "Ursprung als Tupel"
case (0, y):
return f"auf der y-Achse bei {y}"
case [x, y, *rest] if rest:
return f"Sequenz ab {x},{y} plus {len(rest)} weitere"
case Punkt(x=0, y=0):
return "Ursprung als Punkt"
case Punkt(x=x, y=y):
return f"Punkt bei {x},{y}"
case {"typ": "kreis", "radius": r}:
return f"Kreis mit Radius {r}"
case str() as s:
return f"String der Laenge {len(s)}"
case _:
return "unbekannt"
for ding in [(0, 0), (0, 7), [1, 2, 3, 4], Punkt(0, 0), Punkt(3, 4),
{"typ": "kreis", "radius": 2}, "hallo", 42]:
print(beschreibe(ding))
# Output:
# Ursprung als Tupel
# auf der y-Achse bei 7
# Sequenz ab 1,2 plus 2 weitere
# Ursprung als Punkt
# Punkt bei 3,4
# Kreis mit Radius 2
# String der Laenge 5
# unbekannt
Gelesen wird von oben nach unten, der erste passende Fall gewinnt. Drei Details entscheiden darüber, ob das Konstrukt tut, was man erwartet:
Ein nackter Name bindet, er vergleicht nicht.
case (0, y)bindetyan den zweiten Eintrag;case y:allein fängt alles und macht jeden folgenden Fall unerreichbar. Konstanten muss man deshalb qualifizieren —case Farbe.ROT:prüft,case ROT:bindet. Das ist die mit Abstand häufigste Verwechslung.Klassen-Pattern prüfen
isinstanceund dann die Attribute.case Punkt(x=0, y=0)passt nur auf echtePunkt-Objekte. Weil@dataclassautomatisch__match_args__setzt, geht auch die Positions-Kurzformcase Punkt(0, 0).Guards laufen nach dem Zerlegen.
case [x, y, *rest] if rest:zerlegt erst die Sequenz und prüft dann die Zusatzbedingung. Schlägt der Guard fehl, geht es mit dem nächstencaseweiter.
Für Java-Umsteiger: Das entspricht dem Pattern Matching für switch aus Java 21, reicht aber weiter, weil Dictionaries und beliebige Sequenzen dieselbe Syntax benutzen. case _: ist das default. Was in Python fehlt, ist die vom Compiler erzwungene Vollständigkeit — die lässt sich über Enums und einen Typechecker nachrüsten.
Sprachspezifikation if .. elif .. else¶
In der Sprachspezifikation in EBNF ist das if_stmt (“if statement”) definiert:
if_stmt: "if" assignment_expression ":" suite
("elif" assignment_expression ":" suite)*
["else" ":" suite]Dabei sind assignment_expression Ausdrücke, die zu bool evaluiert werden können und suite im Grunde beliebiger Code. Genauer:
assignment_expression: [identifier ":="] expression
expression: conditional_expression | lambda_expr
conditional_expression: or_test ["if" or_test "else" expression]
or_test: and_test | or_test "or" and_test
and_test: not_test | and_test "and" not_test
not_test: comparison | "not" not_test
comparison: or_expr (comp_operator or_expr)*
comp_operator: "<" | ">" | "==" | ">=" | "<=" | "!="
| "is" ["not"] | ["not"] "in"
or_expr: xor_expr | or_expr "|" xor_expr
xor_expr: and_expr | xor_expr "^" and_expr
and_expr: shift_expr | and_expr "&" shift_expr
shift_expr: a_expr | shift_expr ("<<" | ">>") a_expr
a_expr: m_expr | a_expr "+" m_expr | a_expr "-" m_expr
m_expr: u_expr | m_expr "*" u_expr | m_expr "@" m_expr |
m_expr "//" u_expr | m_expr "/" u_expr |
m_expr "%" u_expr
u_expr: power | "-" u_expr | "+" u_expr | "~" u_expr
power: (await_expr | primary) ["**" u_expr]
await_expr: "await" primaryWir sehen also z.B., dass man in einem normalen if auch nicht nur einen bool reinstecken darf, sondern auch eine assignment_expression mit dem sog. Walross-Operator :=:
if x := (42 > 23):
pass
print(x)
# Output: True
Dabei steht pass für ‘Tu nichts’ (die leere Anweisung), die die syntaktische Anforderung eines nicht-leeren Codeblocks erfüllt. Während pass ein Statement ist, gibt es auch den Wert ..., den man auch Ellipsis nennt. Dieser wird bevorzugt als Platzhalter für Werte verwendet, aber je nach Bibliothek verschieden interpretiert (etwa in Numpy oder FastAPI).
Auch sehen wir in dem Auszug der Sprachspezifikation, dass eine expression auch mit dem ternären Operator "if" or_test "else" expression gebildet werden kann. Die Reihenfolge der verschiedenen Operatoren-Deklarationen wie "and" oder "<=" definiert die Präferenz der Auflösung, also welcher Operator wie stark bindet. Wir sehen hier z.B. dass + stärker bindet als |.
Die kleineren Einheiten, die hier mit Operatoren verbunden werden, sind primary:
primary: atom | attributeref | subscription | slicing | call
atom: identifier | literal | enclosure
enclosure: parenth_form | list_display | dict_display | set_display
| generator_expression | yield_atomAnstatt hier weiter die Spezifikation abzugrasen, halten wir fest, dass mit literal z.B. der konkrete String "test" gemeint ist. Ein identifier ist z.B. ein Variablenname. Die Sprachspezifikation ist nützlich, wenn man z.B. nachschauen möchte, was als Variablenname erlaubt ist (z.B. keine Ziffern als erstes Zeichen).
In der Spezifikation von if haben wir noch suite gesehen, das ist formal
suite: stmt_list NEWLINE | NEWLINE INDENT statement+ DEDENT
statement: stmt_list NEWLINE | compound_stmt
stmt_list: simple_stmt (";" simple_stmt)* [";"]
simple_stmt: expression_stmt
| assert_stmt
| assignment_stmt
| augmented_assignment_stmt
| annotated_assignment_stmt
| pass_stmt
| del_stmt
| return_stmt
| yield_stmt
| raise_stmt
| break_stmt
| continue_stmt
| import_stmt
| future_stmt
| global_stmt
| nonlocal_stmt
| type_stmtDiese Liste erinnert nicht umsonst etwas an die Liste an Keywords, die man in Python nicht als identifier verwenden darf:
False await else import pass
None break except in raise
True class finally is return
and continue for lambda try
as def from nonlocal while
assert del global not with
async elif if or yieldSchleifen¶
Eine Schleife hat einen Schleifenkörper, das ist ein Codeblock der ggf. mehrmals ausgeführt wird. Dieser Körper hat keinen eigenen Scope, d.h. Variablen die innerhalb der Schleife deklariert werden, lassen sich außerhalb auslesen (wie bei if).
Eine einzelne Iteration wird abgebrochen mit dem Schlüsselwort continue, die gesamte Schleifeniteration mit break.
while¶
In einer while-Schleife wird zunächst die Bedingung geprüft, und dann ggf. der Schleifenkörper ausgeführt (und dieses Vorgehen dann wiederholt, bis die Bedingung nicht zutrifft).
Im folgenden Beispiel wird eine Codezeile nie erreicht, weil continue bereits abbricht. Die Schleife läuft insgesamt 4 Mal, dabei wird verändert, worauf x zeigt.
x = "test"
while x:
x = x[:-1]
continue
x = "was ganz anderes"
print(x)
# Output:Ein übliches Design-Pattern ist eine while True:-Schleife, in der unter gewissen Bedingungen break ausgelöst wird, oder aber die Schleife durch eine Exception abbricht wie z.B. KeyboardInterrupt wenn der Nutzer mit Ctrl+C abbricht. Das findet sich häufig in Nutzerschnittstellen.
for¶
Die ‘klassische’ for-Schleife mit Index i von i = a bis i = b mit Inkrement +1 lässt sich in Python so schreiben:
a = 1
b = 3
x = 0
for i in range(a,b+1):
x += i
print("1+2+3 =", x)
# Output: 1+2+3 = 6Dabei ist range(a,b) ein spezielles Objekt, welches das Intervall von a (einschließlich) bis b (ausschließend) in den ganzen Zahlen repräsentiert, mit dem Zweck darüber zu iterieren. Es unterscheidet sich von einer Liste dieser Zahlen unter anderem dadurch, dass kein Speicher reserviert werden muss.
r = range(1,4)
l = list(r)
print(r, type(r), l, type(l))
# Output: range(1, 4) <class 'range'> [1, 2, 3] <class 'list'>
Generell kann man in Python über alle Sammlungen iterieren, also ist for ein for-each:
l = [1,2,3]
s = {1,2,3}
d = {1:1, 2:1, 3:99}
for entry in l:
pass
for element in s:
pass
for key in d:
pass
Manchmal möchte man über zwei Sammlungen gleichzeitig iterieren, dazu wäre es verlockend, wiederum einen Index i zu verwenden. Mehr pythonic ist es, die Sammlungen mit zip zu einer zusammen zu fügen:
l = [1,2,3]
s = {"a","b","c"}
x = zip(l,s)
print(type(x), list(x), x)
# Output: <class 'zip'> [(1, 'c'), (2, 'a'), (3, 'b')] <zip object at 0x7fecd4adfb00>
Dabei sehen wir auch, dass die Reihenfolge der Iteration bei der Menge s nicht die selbe ist, wie die in der wir s initialisiert haben (denn Mengen haben keine Anordnung). Generell lässt sich durch Typecasting zu list über den Listen-Konstruktor list(x) gut einsehen, worüber in welcher Reihenfolge iteriert werden würde. Wenn man wirklich einen Index haben möchte, geht das mit enumerate(x).
Es gibt noch mehr Werkzeuge, um komplexere Iterationen zu programmieren, etwa im Modul itertools der Standard-Bibliothek.
list comprehensions¶
Die for-Notation lässt sich auch zum Bilden neuer Listen (und Mengen und Dictionaries) verwenden.
Anstelle dieses unpythonic Codes:
s = {"a","b","c"}
l = list()
for element in s:
l.append(element + "...")
print(l)
# Output: ["c...", "a...", "b..."]
lässt sich deutlich lesbarer schreiben:
s = {"a","b","c"}
l = [element + "..." for element in s]
print(l)
# Output: ["c...", "a...", "b..."]
Generator Expressions¶
Die List-Comprehension legt die vollständige Liste im Speicher an. Wenn diese nur genutzt wird, um darüber zu iterieren, ist das nicht zwingend erforderlich. Python bietet die Möglichkeit der lazy evaluation in der sozusagen die Listenbildungsvorschrift gespeichert wird, das nächste Listenelement aber erst auf Abruf erzeugt wird (runde Klammern statt eckige). Anstelle dieses speicherhungrigen Codes:
s = {"a","b","c"}
l = [element + "..." for element in s]
processed_list = [len(element) for element in l]
x = sum(processed_list)
print(x)
# Output: 12
lässt sich deutlich eleganter schreiben:
s = {"a","b","c"}
g = (element + "..." for element in s)
processed_generator = (len(element) for element in g)
x = sum(processed_generator)
print(x)
# Output: 12
print(type(g), g)
# Output: <class 'generator'> <generator object <genexpr> at 0x7fecd4df8110>
Iterable und Iterator¶
Allgemein nennt man eine Art von Ding (z.B. eine Sammlung), über die man iterieren (d.h. eine Schleife laufen lassen) kann, ein iterable und den Teil des Programms, der das tatsächliche darüber-Schleifen-lassen und buchführen, wo der Zeiger gerade hinzeigt, den iterator.
In Python ist jedes Objekt ein iterable, wenn __iter__() implementiert ist, eine Methode, die einen Iterator zurückgeben soll. Beispiele: list, str, dict, set, tuple, range()-Objekte.
Ein Iterator ist jedes Objekt, welches __next__() implementiert hat. Wenn es keine Items mehr gibt, über die iteriert werden kann, wirft diese Methode eine StopIteration-Exception. Gemäß dem Grundsatz “It’s Easier to Ask for Forgiveness than Permission” fragt man nicht, ob es noch Items gibt, man lässt sich eins geben (und fängt ggf. eine Exception auf; das macht die for-Schleife automatisch).
Dieses Protokoll lässt sich selbst implementieren; mehr als die beiden Methoden braucht es nicht:
class Countdown:
def __init__(self, start):
self.rest = start
def __iter__(self):
return self # dieses Objekt ist sein eigener Iterator
def __next__(self):
if self.rest <= 0:
raise StopIteration
self.rest -= 1
return self.rest + 1
for zahl in Countdown(3):
print(zahl, end=" ")
# Output: 3 2 1
Wer in Java Iterable<T> implementiert hat, erkennt das Muster wieder — mit dem Unterschied, dass hier nichts deklariert und nichts geerbt wird: Es genügt, dass die Methoden existieren (Duck Typing). Dass __iter__ hier self zurückgibt, ist eine verbreitete Abkürzung; sie hat den Preis, dass ein Countdown(3) nach einem Durchlauf verbraucht ist. Wer dieselbe Sammlung mehrfach (oder verschachtelt) durchlaufen können will, gibt in __iter__ ein frisches Iterator-Objekt zurück — genau das tut list.
Fast immer ist der kürzere Weg aber eine Generator-Funktion: Ein yield im Funktionskörper genügt, damit Python __iter__, __next__ und das StopIteration selbst erzeugt. Die Funktion merkt sich dabei ihren Zustand zwischen zwei Aufrufen (an welcher Zeile sie stand, welche lokalen Variablen sie hatte) — das ist dieselbe Idee wie bei den Closures weiter unten.
def countdown(start):
while start > 0:
yield start
start -= 1
print(list(countdown(3)), type(countdown(3)))
# Output: [3, 2, 1] <class 'generator'>
Das ist die gleiche lazy evaluation wie bei den Generator Expressions oben, nur mit beliebigem Code statt einem einzigen Ausdruck. Auf diesem Protokoll baut später das kooperative Multitasking mit async/await auf (siehe Asyncio); auch yield gibt die Kontrolle ab und nimmt sie später an derselben Stelle wieder auf.
Funktionen¶
Um Methoden (Funktionen) zu definieren, dient das Schlüsselwort def. Eine sehr kurze Methode ist
def x():
pass
print(x, type(x), x())
# Output: <function x at 0x7fecd4b000e0> <class 'function'> None
Während if und for keinen eigenen Scope erzeugen, erzeugt def einen eigenen Scope:
a = 5
def x():
a = 10
return a
b = x()
print(a, b)
# Output: 5 10
Eine Methode kann Argumente annehmen. Es gibt optionale Argumente und Keyword-Argumente:
def f(mandatory, mandatory2, optional="default", optional2="another"):
print(mandatory2, optional2)
f(1, 2, optional2=3)
# Output: 2 3
Man kann auch deklarieren, dass beliebige weitere Argumente und Keyword-Argumente mit übergeben werden dürfen:
def f(mandatory, *args, optional="default", **kwargs):
print(args, kwargs)
f(1, 2, 4, 5, 6, optional="different", optional_extra="und mehr")
# Output: (2, 4, 5, 6) {'optional_extra': 'und mehr'}
dabei ist args ein Tupel und kwargs ein Dictionary. Die Konstruktion , **kwargs) in den Methodensignatur ist ein übliches Design-Pattern, um flexibel zusätzliche Parameter durchzureichen.
Da die Parameternamen bei Keyword-Argumenten beim Aufruf der Funktion verwendet werden, sorgen sie für größere Lesbarkeit. Wenn man bei einem Argument ohne default-Wert erzwingen möchte, dass dieses im Keyword-Stil übergeben werden muss, kann man dies mit einem * in der Methodensignatur erreichen:
def f(mandatory, *, keyword_without_default):
print(keyword_without_default, mandatory)
f(23, keyword_without_default=42)
# Output: 42 23
Die notwendigen Argumente, die nicht im Keyword-Stil übergeben werden, heißen positional arguments. Das hilft beim Debuggen, wenn die Fehlermeldung etwa TypeError: f() takes 1 positional argument but 2 were given lautet.
Achtung: Code in der Deklaration einer Methode wird direkt ausgeführt, nicht erst beim Aufrufen der Methode; das gilt insbesondere für die Zuweisung von Default-Werten für Keyword-Argumente:
def add(item, box=[]): # Oh!
box.append(item)
return box
print(add("A")) # Output: ['A']
print(add("B")) # Output: ['A', 'B']
Der Code box=[] wird genau ein mal ausgeführt, danach ist ein Objekt festgelegt, welches als Default-Wert für das Argument box verwendet wird bei jedem Aufruf ohne explizites box-Argument.
Wenn man bei jedem Aufruf von add ohne box-Argument eine neue frische list-Instanz anlegen möchte, so muss man das im Körper der Methode tun, etwa so:
def add(item, box=None):
if box is None: box = []
box.append(item)
return box
print(add("A")) # Output: ['A']
print(add("B")) # Output: ['B']
print(add("D", box=add("C")))
# Output: ['C', 'D']
Sentinel values¶
Ein Konzept beim Design von APIs sind Sentinel Values (Wächter-Werte). Wenn in einer Methodensignatur der Wert None ein inhaltlich gültiger Input ist, lässt sich None nicht mehr verwenden, um festzustellen, ob ein Aufrufer das Argument bewusst übergeben oder komplett weggelassen hat. In solchen Fällen definiert man ein eindeutiges Sentinel-Objekt:
MISSING = object()
def update_value(new_value=MISSING):
if new_value is MISSING:
print("Nichts zu tun, kein Wert übergeben.")
else:
print("Update auf:", new_value)
update_value(None) # Output: Update auf: None
update_value() # Output: Nichts zu tun, kein Wert übergeben.In Python 3.15 kann man (PEP 661) statt einem reinen object auch spezifizieren, dass man es mit einem Sentinel value zu tun hat (damit Type Checker es leichter haben):
MISSING = sentinel("MISSING", repr="<MISSING>")
Tuple Unstructuring¶
Python erlaubt Variablenzuweisung mit implizitem Auspacken:
t = (1, 2, 3)
x, y, z = t
print(x, y, z, t)
# Output: 1 2 3 (1, 2, 3)
t = (1, (2, 3))
x, (y, z) = t
print(x, y, z, t)
# Output: 1 2 3 (1, (2, 3))
Das lässt sich auch sehr gut in Schleifen verwenden:
ltt = [(1,(2,3)), (4,(5,6))]
for x, (y,z) in ltt:
print(x, y, z)
# Output:
# 1 2 3
# 4 5 6
Ähnlich wie *args in der Methodensignatur lässt sich der Stern-Operator auch andernorts verwenden:
head, *body, tail = [1, 2, 3, 4, 5]
print(*body)
# Output: 2 3 4
Während * generell auf Iterables arbeitet, ist ** das Analogon für Mappings, also Dictionaries:
data = (1, 2, 3)
options = {"sep": "-"}
print(*data, **options)
# Output: 1-2-3
Ab Python 3.15 lässt sich der Entpackungs-Operator * direkt innerhalb von Comprehensions verwenden (PEP 798); in älteren Versionen nimmt man dafür itertools.chain.from_iterable. Dies ermöglicht ein sehr elegantes Flattening von verschachtelten iterierbaren Objekten, ohne auf komplizierte, verschachtelte Schleifenkonstrukte zurückgreifen zu müssen:
listen = [[1, 2], [3, 4], [5]]
abgeflacht = [*it for it in listen]
print(abgeflacht)
# Output: [1, 2, 3, 4, 5]
Analog für Dictionaries:
dicts = [{"a": 1}, {"b": 2}]
merged = {**d for d in dicts}
print(merged)
# Output: {'a': 1, 'b': 2}
Lambda¶
Wenn man eine namenlose Funktion hat, die in eine Zeile passt, ist anstelle von def oft auch ein Lambda-Ausdruck eine gute Wahl. Man kann solche Ausdrücke einer Variablen zuweisen:
f = lambda x, y: x + y
print(f(2,3), f)
# Output: 5 <function <lambda> at 0x7fecd4b01e40>
Auch ohne Namen lässt sich der Lambda-Ausdruck ausführen:
print( (lambda x, y: x + y)(2, 3) )
# Output: 5
Achtung: Lambda-Ausdrücke erlauben kein generisches Tuple Unstructuring, also ist lambda x, (y,z): x kein gültiger Code.
Häufig begegnet man solchen Lambda-Ausdrücken, wenn man die Methode sort benutzt, um einen Komparator zu definieren oder wenn man map und filter benutzt. Wer Design-Patterns aus der funktionalen Programmierung oder Kombinatorlogik verwenden möchte, kann dazu gut Lambda-Expressions benutzen. Weil wegen dieses Konstrukts lambda ein reservierter Ausdruck ist, wird für den griechischen Buchstaben gelegentlich als Identifier lamda verwendet (im Sinne von alpha, beta, lamda). Das ist dann sozusagen ein absichtlicher Typo (Typographical error, Tippfehler).
Grundsätzlich müssen Lambda-Ausdrücke keine Einzeiler sein:
step1 = step2 = step3 = lambda x : x + 1
multistep = lambda inputvalue: (res1 := step1(inputvalue),
res2 := step2(res1),
step3(res2))[-1]
print(multistep(3), multistep(4))
# Output: 6 7Scope und Closures¶
Es gibt in Python 4 Scopes (Anwendungsbereich trifft es am ehesten als deutsche Übersetzung): Local, Enclosed, Global, Built-in.
Wenn Python nach einer Variablen (wie n in x * n) sucht, werden in der Reihenfolge LEGB diese Scopes durchsucht. Beim Zuweisen eines Werts wird stets eine neue lokale Variable im aktuellen Scope erstellt. Mit dem Schlüsselwort global lässt sich deklarieren, dass man eine Variable aus dem globalen Scope meint. Das Schlüsselwort nonlocal hingegen sucht die Variable im nächstgelegenen umschließenden Funktions-Scope. Es wird nur bei verschachtelten Funktionen verwendet wie im Beispiel:
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
counter = make_counter()
print(counter(), counter(), counter())
# Output: 1 2 3
In Python kann man Methoden definieren, die zusätzlich einen Zustand erinnern (closures):
def make_mul(n):
def mul(x):
return x * n
return mul
mul_10 = make_mul(10)
mul_2 = make_mul(2)
print(mul_10(5), mul_2(5))
# Output: 50 10
In dem Beispiel ist mul_10 eine Methode, die n = 10 erinnert. Eine alternative Implementierung würde die Variable n als Eigenschaft (property) eines Objekts verwalten.
Diese Programmkonstrukte sind für Anfänger mit einem objektorientierten Hintergrund vielleicht nicht so naheliegend, aber bieten mit Python eine leichtgewichtigere Alternative zu objektorientierten Design-Patterns um Zustand zusammen mit Methoden zu verwalten.
Zustand: global ist fast nie die Antwort¶
Weil eine Zuweisung im Funktionskörper standardmäßig eine lokale Variable anlegt, läuft der naheliegende Versuch, einen Zähler hochzuzählen, in einen UnboundLocalError. Das Schlüsselwort global behebt den Fehler — und schafft einen größeren:
punkte = 0
def treffer():
global punkte # ohne diese Zeile: UnboundLocalError
punkte += 1
treffer()
treffer()
print(punkte)
# Output: 2
Der Zustand hängt jetzt am Modul, nicht am Spiel. Damit gibt es die Partie genau einmal pro Prozess: Ein zweites Spiel parallel ist unmöglich, ein Test kann den Ausgangszustand nicht setzen, und die Reihenfolge der Tests wird plötzlich relevant, weil jeder Test die Punkte des vorigen erbt.
Die Alternative kostet eine Zeile mehr und löst alle drei Probleme auf einmal:
from dataclasses import dataclass
@dataclass
class Spiel:
punkte: int = 0
def treffer(self) -> None:
self.punkte += 1
spiel = Spiel()
spiel.treffer()
spiel.treffer()
print(spiel.punkte, Spiel().punkte)
# Output: 2 0
Spiel() erzeugt so oft einen frischen Ausgangszustand, wie man ihn braucht — im Test genauso wie im Programm. Als Faustregel: global ist in Anwendungscode ein Warnsignal. Legitim bleibt es für Konstanten (die man ohnehin nicht zuweist) und für bewusst prozessweite Register wie Logging-Konfiguration. Wie sich der Unterschied im Test auswirkt, steht im Kapitel Testing; die saubere Trennung von Zustand und Darstellung zeigt practical-1.
Dekoratoren¶
Ein Dekorator ist nichts weiter als eine Funktion, die eine Funktion entgegennimmt und eine Funktion zurückgibt. Weil Funktionen in Python gewöhnliche Objekte sind (man kann sie herumreichen, in Listen stecken und neu zuweisen), braucht es dafür keinen Sprachmechanismus — nur Closures.
Ein Dekorator, der jeden Aufruf protokolliert:
def laut(funktion):
def verpackung(*args, **kwargs):
print("->", funktion.__name__, "mit", args)
ergebnis = funktion(*args, **kwargs)
print("<-", funktion.__name__, "liefert", ergebnis)
return ergebnis
return verpackung
def addiere(x, y):
return x + y
addiere = laut(addiere) # das Original wird ersetzt
addiere(2, 3)
# Output: -> addiere mit (2, 3)
# <- addiere liefert 5
Die Zeile addiere = laut(addiere) ist der ganze Trick. Weil dieses Muster so häufig vorkommt, gibt es dafür die @-Syntax; sie bedeutet exakt dasselbe, steht aber am Kopf der Definition, wo man sie beim Lesen nicht übersieht:
@laut
def addiere(x, y):
return x + y
Hier lauert die wichtigste Falle: Der zurückgegebene Wrapper ist ein anderes Objekt als das Original, und er trägt dessen Namen und Docstring nicht. functools.wraps kopiert diese Metadaten (__name__, __doc__, __wrapped__, Type Hints) nach vorn — in eigenen Dekoratoren sollte man das immer tun, sonst zeigen Debugger, help() und Doctests ins Leere:
import functools
def laut(funktion):
@functools.wraps(funktion)
def verpackung(*args, **kwargs):
return funktion(*args, **kwargs)
return verpackung
@laut
def addiere(x, y):
"""Addiert zwei Zahlen."""
return x + y
print(addiere(2, 3), addiere.__name__, addiere.__doc__)
# Output: 5 addiere Addiert zwei Zahlen.
Ohne @functools.wraps stünde dort verpackung None.
Soll der Dekorator selbst Argumente bekommen (@wiederhole(3)), braucht es eine Ebene mehr: eine Funktion, die den Dekorator zurückgibt, der dann die Funktion ersetzt.
def wiederhole(anzahl):
def dekorator(funktion):
@functools.wraps(funktion)
def verpackung(*args, **kwargs):
for _ in range(anzahl):
ergebnis = funktion(*args, **kwargs)
return ergebnis
return verpackung
return dekorator
@wiederhole(3)
def piep():
print("piep", end=" ")
piep()
# Output: piep piep piep
Für Java-Umsteiger ist die Abgrenzung wichtig: Eine Java-Annotation wie @Override ist Metadaten, die ein Compiler oder eine Bibliothek per Reflection auswertet — am Verhalten der Methode ändert sie von sich aus nichts. Ein Python-Dekorator ist ausführbarer Code, der zur Definitionszeit läuft und das Objekt tatsächlich austauscht. Nach @laut ist addiere nicht mehr die Funktion, die dort im Quelltext steht.
Dekoratoren begegnen uns ab hier überall im Skript, und es lohnt sich, sie beim Lesen als das zu sehen, was sie sind:
| Dekorator | Kapitel | was er tut |
|---|---|---|
@property, @staticmethod, @classmethod | Objektorientierung | ersetzt die Methode durch ein Deskriptor-Objekt |
@dataclass | Objektorientierung | erzeugt __init__, __repr__, __eq__ und hängt sie an die Klasse |
@functools.cache | – | legt einen Ergebnis-Cache vor die Funktion |
@pytest.fixture, @pytest.mark.parametrize | Testing | meldet die Funktion beim Test-Framework an |
@icontract.require, @deal.pre | Design by Contract | prüft Vor-/Nachbedingungen bei jedem Aufruf |
@app.get("/pfad") | User Interfaces | trägt die Funktion in eine Routing-Tabelle ein |
Auch Klassen lassen sich dekorieren (@dataclass ist genau das: eine Funktion, die eine Klasse nimmt und eine Klasse zurückgibt).
Nützliches aus functools¶
Drei Funktionen aus dem Modul functools begegnen einem ständig. functools.wraps kennen wir schon; die beiden anderen sind Dekorator bzw. Funktionsfabrik.
@functools.cache legt einen unbegrenzten Ergebnis-Cache vor die Funktion. Aus einer exponentiell teuren Rekursion wird damit eine lineare, ohne dass man den Algorithmus anfasst:
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(70))
print(fib.cache_info())
# Output: 190392490709135
# CacheInfo(hits=68, misses=71, maxsize=None, currsize=71)
Die Bedingungen dafür sind streng: Die Argumente müssen hashbar sein, und die Funktion muss rein sein, also für gleiche Argumente immer dasselbe liefern und nichts nebenbei verändern. Wer eine Funktion cached, die eine Datei liest, cached den alten Dateiinhalt. functools.lru_cache(maxsize=…) ist die Variante mit Obergrenze; @cache ist lru_cache(maxsize=None).
functools.partial belegt Argumente vor und gibt eine neue aufrufbare Funktion zurück — das ist dieselbe Idee wie die Closure aus dem Abschnitt oben, nur ohne eigene Funktionsdefinition:
from functools import partial
def potenz(basis, exponent):
return basis ** exponent
quadrat = partial(potenz, exponent=2)
print(quadrat(7), quadrat(12))
# Output: 49 144
Typisch ist das als Callback: button.on_click(partial(setze_farbe, "rot")) übergibt eine Funktion, die schon weiß, welche Farbe gemeint ist. In Java bräuchte man dafür ein Lambda mit effectively final-Variable; partial macht die Bindung explizit.
Try With ...¶
Wenn man bei einer Operation einen bestimmten Fehler erwartet, ist das try-except-else-finally-Konstrukt geeignet. Der else-Block und der finally-Block sind optional. Man kann mehrere except-Blöcke hintereinander schreiben.
try:
result = 10 / 0
except ZeroDivisionError as e:
print("There are problems:", e)
else:
print("The try-block succeeded.")
finally:
print("This always runs.")
# Output:
# There are problems: division by zero
# This always runs.
Wenn man z.B. in eine Datei schreibt oder über ein Netzwerk in eine Datenbank schreibt, kann es zu Input/Output-Fehlern kommen. Ein eleganter Weg, die Datei bzw. den Datenbankzugriff wieder zu schließen nach der Nutzung, unabhängig davon ob es zu Fehlern kam, ist das with-as-Konstrukt:
with open("filename.txt", "w") as file:
file.write("Hallo")
Hierbei ruft Python auf file automatisch close() auf, nachdem der with-Block ausgeführt wurde. Das ganze ist syntaktischer Zucker für das Context Manager Protocol, welches die dunder Methoden __enter__() und __exit__() vorschreibt.
Man könnte denken, dass das as-Schlüsselwort einen neuen Scope eröffnet, aber wie schon bei Schleifen und if-Verzweigungen ist das nicht der Fall; hier ist konkret file auch nach dem with-Statement noch verfügbar.
Ein typisches Design-Pattern ist, ein with open([...]) as file: in ein try..except zu verpacken, um z.B. FileNotFoundError abzufangen. Dabei ist try für Fehlerbehandlung und with für das Resourcenmanagement zuständig.
Eigene with-Blöcke¶
Das Context-Manager-Protokoll besteht nur aus __enter__ und __exit__, trotzdem muss man dafür keine Klasse schreiben. contextlib.contextmanager macht aus einer Generator-Funktion einen Context Manager: Alles vor dem yield ist __enter__, alles danach ist __exit__, und der Wert hinter dem yield landet im as.
import contextlib
@contextlib.contextmanager
def klammer(zeichen):
print(zeichen[0], end="")
try:
yield zeichen
finally:
print(zeichen[1], end="")
with klammer("[]") as z:
print("Inhalt", end="")
print()
# Output: [Inhalt]
Das try ... finally um das yield herum ist nicht schmückendes Beiwerk, sondern der Punkt der Übung: Nur so läuft der Aufräumteil auch dann, wenn der with-Block eine Exception wirft. Ohne finally hätte man einen Context Manager gebaut, der genau im Fehlerfall nicht aufräumt — also in dem Fall, für den es ihn gibt.
Zwei fertige Helfer aus demselben Modul ersparen oft die eigene Definition: contextlib.suppress(FehlerTyp) ersetzt ein try/except/pass, und contextlib.ExitStack verwaltet eine zur Laufzeit variable Zahl von Context Managern (etwa “öffne alle Dateien aus dieser Liste”).
import contextlib
with contextlib.suppress(ZeroDivisionError):
1 / 0
print("weiter")
# Output: weiter
Dekoratoren und Context Manager treffen sich hier übrigens: @contextlib.contextmanager ist selbst ein Dekorator, und das Ergebnis lässt sich mit @meinmanager() sogar als Dekorator um ganze Funktionen legen.
Zusammenfassung und Kontrollfragen¶
matchvergleicht nicht nur, es zerlegt — und ein nackter Name in einemcasebindet immer und vergleicht nie; das ist die häufigste Fehlerquelle des Konstrukts.Comprehensions bauen die vollständige Sammlung im Speicher, Generator Expressions liefern auf Abruf: gleiche Syntax, völlig unterschiedlicher Speicherbedarf.
Das Iteratorprotokoll besteht aus
__iter__und__next__; eine Generator-Funktion mityielderzeugt beides, ohne dass man es schreibt.Ein Dekorator ist eine Funktion, die eine Funktion nimmt und eine Funktion zurückgibt; ohne
functools.wrapsverliert das Ergebnis Name und Docstring, und Debugger,help()und Doctests zeigen ins Leere.Zustand gehört in ein Objekt, nicht in eine Modulvariable:
globalbehebt denUnboundLocalErrorund schafft dafür ein Programm, das es nur einmal pro Prozess gibt und das sich nicht testen lässt.
Kontrollfragen
Warum fängt
case farbe:jeden beliebigen Fall ab? Wie schreiben Sie es, wenn Sie gegen eine Konstante vergleichen wollen?Sie wollen die Summe aller Zeilenlängen einer Datei mit einer Million Zeilen. Warum ist
sum([len(z) for z in datei])schlechter alssum(len(z) for z in datei)?Ein Kollege hat einen Dekorator geschrieben; seither liefert
help()für alle damit dekorierten Funktionen nichts Sinnvolles mehr. Was fehlt in seinem Dekorator, und warum?