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.
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
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 pass, 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 pass bereits abbricht. Die Schleife läuft insgesamt 4 Mal, dabei wird verändert, worauf x zeigt.
x = "test"
while x:
x = x[:-1]
pass
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).
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']
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> <class 'function'>
Auch ohne Namen lässt sich der Lambda-Ausdruck ausführen:
print( (lambda x, y: x + y)(2, 3) )
# Output: 5
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).
Scope und Closures¶
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.
Wenn Python nach einer Variablen (wie n in x * n) sucht, ist die Reihenfolge local, enclosing, global, built-in. 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
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.
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\nThis 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.