Skip to main content

Häufige Python Fauxpas

Viele Codebeispiele für Anfänger zeigen nur vereinfachten Python-Code und lehnen sich dabei an altbewährte Programmabläufe an (z.B. wie man sie bereits aus C kennt). Wendet man diesen Code häufig an, können sich auch für fortgeschrittene Programmierer Gewohnheiten einstellen, die dazu führen, dass der geschrieben Code fehleranfällig wird oder garnicht das mach, was er soll.

Durch meine Fehler konnte ich viel Erfahrung sammeln und führe aus dieser schöpfend in diesem Beitrag Fehlangewohnheiten auf, die meiner Ansicht nach beim Programmieren in Python häufig gemacht werden, sich jedoch leicht vermeiden lassen.

Formatierung einer Zeichenkette

Um Zeichenketten mit Werten von Variablen zu versehen wird oft gezeigt, dass man Zeichenketten mit dem Operator + verbinden kann:


def string_formatting_bad(name: str, subscriber: int) -> None:
    if subscriber < 100000:
        print(name + " does not have 100k subscribers yet.")
    else:
        print("Amazing! " + name + " has " + str(subscriber) + " subscribers!")

Jedoch ist es empfehlenswerter, die Zeichenkette mit der eingebauten Methode format() zu formatieren, oder die Zeichenkette selbst als formatierte Zeichenkette (also mit f vor den Anführungszeichen) zu kennzeichnen, sodass die Variablen in geschweiften Klammern direkt eingebettet werden können:


def string_formatting_good(name: str, subscriber: int) -> None:
    if subscriber < 100000:
        print(f"{name} does not have 100k subscribers yet.")
    else:
        print(f"Amazing! {name} has {subscriber} subscribers!")

Dateien richtig schließen

Angelehnt an grundlegenden Programmiersprachen wie C, wird oft gezeigt, dass Dateien nach dem Öffnen wieder (explizit) geschlossen werden müssen.


def using_a_file_bad(file_name: str) -> None:
    f = open(file_name, "w")
    f.write("Hello, world!\n")
    f.close()

Bricht der Code allerdings vor dem Schließen ab (z.B. weil beim Schreiben ein Fehler auftritt), so wird die Datei nie geschlossen. Daher empfiehlt es sich, die Datei im Rahmen einer Kontextverwaltung zu öffnen. Dies geschieht mit dem Schlüsselwort with, wobei der Kontext (hier die geöffnete Datei) durch as einer Variable zugewiesen wird, die man dann innerhalb des nachstehenden Codeabschnitts benutzen kann. Wird der Codeabschnitt verlassen (regelmäßig oder aufgrund eines Fehlers) wird der Kontext automatisch beendet (die Datei also geschlossen).


def using_a_file_good(file_name: str) -> None:
    with open(file_name, "w") as f:
        f.write("Hello, world!\n")

Kontextverwaltung

Man kann eine Kontextverwaltung mit auch mit der Kombination aus try und finally erwirken:


def using_context_manager_bad(host: str, port: int) -> None:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.connect((host, port))
        s.sendall(b"Hello, world!")
    finally:
        s.close()

Jedoch besteht auch hier die Möglichkeit, dass man vergisst, den Context (hier einen Socket) zu schließen. Die Kontextverwaltung mittels with und as erledigt das von selbst und führt zu lesbarerem Code.


def using_context_manager_good(host: str, port: int) -> None:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((host, port))
        s.sendall(b"Hello, world!")

Richtiger Umgang mit Exception

Will man vermeiden, dass der Code aufgrund eines Fehlers abbricht, so kann man die Kombination der Schlüsselworte try und catch benutzen, um Fehler aufzufangen:


def exception_bad() -> None:
    while True:
        try:
            s = input("Please enter a number: ")
            print(f"You entered the number: {int(s)}")
            break
        except:
            print(f"You did not enter a number")

Definiert man jedoch nicht explizit, welche Fehler gefangen werden sollen, so kann der Benutzer das Programm nicht über strg+c verlassen. Daher sollte man mindestens angeben, dass man mindestens einen codebasierten Fehler auffangen will:


def exception_good() -> None:
    while True:
        try:
            s = input("Please enter a number: ")
            print(f"You entered the number: {int(s)}")
            break
        except Exception:
            print(f"You did not enter a number")

Bestenfalls gibt man jedoch explizit den konkreteren Datentyp des Fehlers an, den man auffangen will:


def exception_better() -> None:
    while True:
        try:
            s = input("Please enter a number: ")
            print(f"You entered the number: {int(s)}")
            break
        except ValueError:
            print(f"You did not enter a number")

Falscher Operator für Exponentialrechnung

Andere Programmiersprachen benutzen des Öfteren den Operator ^, um ein Exponential zu berechnen. Bei Python ist dies jedoch der XOR-Operator:


def square_bad(a: int) -> int:
    return a ^ 2

Statt ^ kennzeichnen in Python zwei Sternchen ** die Exponentialrechnung:


def square_good(a: int) -> int:
    return a ** 2

Mutierbare Argumente als Standardwert

Will man einen Standardwert definieren, sodass der Benutzer beim mehrmaligen Aufruf einer Funktion auf die wiederholte Angabe dieses Standardwerts verzichten kann, so hätte man wahrscheinlich folgenden Code geschrieben:


def append_bad(n: int, l: list = []) -> list:
    l.append(n)
    return l

l_1 = append_bad(0)
l_2 = append_bad(1)

Jedoch wird hierbei ein Standardwert definiert, der veränderbar bzw. mutierbar ist. Somit enthält l_2 am Ende den Wert [0, 1]. Denn Standardwerte für Argumente einer Funktion werden mit der Definition der Funktion selbst definiert und nicht, wenn die Funktion ausgeführt wird. Für das vorstehende Beispiel bedeutet das, dass sich jeder Aufruf der Funktion dieselbe Liste l teilt, und dass diese beim zweiten Aufruf der Funktion bereits den Wert [0] innehat. Um dies zu vermeiden, sollte man mutierbare Standardwerte auf None setzen und innerhalb der Funktion (bei Laufzeit) definieren:


def append_good(n: int, l: list = None) -> list:
    if l is None:
        l = []
    l.append(n)
    return l

Listenzusammenfassung

Will man eine Datenliste mit Werten füllen, könnte man auf die Idee kommen, bei einer Liste (bzw. list) jeden Wert einzeln hinzuzufügen oder bei einem Directory (bzw. dict) die Werte einzeln zuzuweisen. Somit würde sich wahrscheinlich der folgende Code als eine Zusammenfassung (bzw. einer Comprehension) ergeben:


def list_comprehension_bad() -> None:
    numbers = []
    for i in range(10):
        numbers.append(i)

    squares = {}
    for i in numbers:
        squares[i] = i * i

Jedoch ist dieser einfache Code sehr lang und schlecht lesbar. Daher gibt es in Python die sogenannte Listenzusammenfassung (bzw. die List Comprehension) mit der der vorstehende Code auf zwei Zeilen (ohne die Funktionsdefinition) reduziert werden kann:


def list_comprehension_good() -> None:
    numbers = [i for i in range(10)]
    squares = {i: i for i in numbers}

Listenzusammenfassung wo nötig Umfang

Zwar soll die Listenzusammenfassung eine vereinfachte Schreibweise für die Definition von Datenlisten anbieten, man kann sie aber auch missbrauchen:


def list_comprehension_bad(a: list[int], b: list[int], n: int) -> list[int]:
    return [
        sum(a[n * i * k] * b[n * k + j] for k in range(n))
        for i in range(n)
        for j in range(n)
    ]

Hier soll nur das Produkt von zwei Matrizen a und b berechnet werden, die jeweils ein Größe von n x n haben. Allerdings ist dies nicht schnell ersichtlich. Daher ist es in diesem Fall vorzuziehen, auf die übermäßige und verschachtelte Nutzung von Listenzusammenfassung zu verzichten und die klassische for-Schleife zu verwenden:


def list_comprehension_good(a: list[int], b: list[int], n: int) -> list[int]:
    c = []
    for i in range(n):
        for j in range(n):
            ij = sum(a[n * i * k] * b[n * k + j] for k in range(n))
            c.append(ij)
    
    return c

Abgleich des Datentyps

Grade bei einer objektorientierten Programmiersprache wie Python, muss gelegentlich Code in Abhängigkeit vom Typ einer Variable ausgeführt werden. Die Funktion type() gibt hier den Namen des Datentyps als Zeichenkette zurück:


from collections import namedtuple

def type_comparison_bad() -> None:
    Point = namedtuple("Point", ["x", "y"])
    p = Point(3.5, 7.4)

    if type(p) == tuple:
        print("This is a tuple")
    else:
        print("It is not a tuple")

Allerdings kann dieser Code, z.B. bei vererbten Klassen oder Datentypen mit Bezeichnungen, die nicht dem zugrundeliegen Datentyp entsprechen, keine Gleichheit feststellen. Es kommt hierbei nicht auf die Gleichheit als solche an, sonder darauf, dass das Objekt, das hinter der Variable steckt, die Funktion des gewünschten Datentyps hat. Das wird eben nicht mit der Gleichheit, sondern mit der Funktion isinstance() geprüft.


from collections import namedtuple

def type_comparison_good() -> None:
    Point = namedtuple("Point", ["x", "y"])
    p = Point(3.5, 7.4)

    if isinstance(p, tuple):
        print("This is a tuple")
    else:
        print("It is not a tuple")

Vergleich mit Null, Wahr und Falsch

Will man sichergehen, dass eine Variable gesetzt (also nicht Null) ist und den Wert Wahr oder Falsch hat, könnte man dies auch mit dem Vergleichoperator == machen:


def singleton_comparison_bad(x) -> None:
    if x == None:
        pass
    if x == True:
        pass
    if x == False:
        pass

Allerdings sollte hierbei nicht die Gleichheit als solche mit ==, sondern die Identität mit is geprüft werden.


def singleton_comparison_good(x) -> None:
    if x is None:
        pass
    if x is True:
        pass
    if x is False:
        pass

Unnötige Längen- oder boolesche Vergleiche

Um zu prüfen, ob eine Variable gesetzt oder eine Liste gefüllt ist, könnte man auf die Funktionen bool() und len() zurückgreifen:


def check_bad(x) -> None:
    if bool(x):
        pass

    if len(x) != 0:
        pass

Allerdings kann diese Abfrage auch wesentlich einfacher formuliert werden, nämlich indem die if-Bedingung unmittelbar auf die Variable angewandt wird:


def check_good(x) -> None:
    if x:
        pass

Schleifen über eine Liste

Aus Programmiersprachen wie C kennt man vielleicht schon Arrays und die Syntax, mit der Werte aus einem Array ausgelesen werden können. Dieselbe Syntax, also den Index in eckigen Klammern auf die Liste anzuwenden, funktioniert auch in Python:


def loop_over_list_bad(x: list) -> None:
    for i in range(len(x)):
        print(x[i])

Will man allerdings nur die Werte extrahieren, so ergibt es mehr Sinn, die for-Schleife unmittelbar auf die Liste anzuwenden:


def loop_over_list_good(x: list) -> None:
    for v in x:
        print(v)

Benötigt man neben den Werten auch die Indexe, so gibt es hierfür die Funktion enumerate():


def loop_over_list_better(x: list) -> None:
    for i, v in enumerate(x):
        print(f"@{i} = {v}")

Schleife über mehrere Listen

Beim Durchlaufen mehrerer Listen, könnte man auf die Idee kommen, einen gemeinsamen Zähler für die Indizierung zu benutzen:


def loop_over_lists_bad(x: list, y: list) -> None:
    for i in range(len(x)):
        print(x[i])
        print(y[i])

Allerdings kann man mit der Funktion zip() zwei gleichlange Listen gemeinsam durchlaufen und deren Werte (auf derselben Indexhöhe) an die for-Schleife übergeben:


def loop_over_lists_good(x: list, y: list) -> None:
    for vx, vy in zip(x, y):
        print(vx)
        print(vy)

Genau wie vorstehend bereits beschrieben, kann die Funktion enumerate() neben den Werten auch den Index für mehrere Listen bereitstellen.


def loop_over_lists_better(x: list, y: list) -> None:
    for i, (vx, vy) in enumerate(zip(x, y)):
        print(f"@{i} = {vx}")
        print(f"@{i} = {vy}")

Schleife über ein Dictionary

Will man eine Schleife über ein Dictionary laufen lassen, funktioniert das nicht mit einem fortlaufenden Index, sondern man muss auf die Schlüsselworte im Dictionary zurückgreifen:


def loop_oper_dict_bad(d: dict) -> None:
    for k in d.keys():
        print(f"Key: {k}")

Allerdings läuft die for-Schleife automatisch über die Schlüsselworte des Dictionary, sodass auf die explizite Angabe mittels keys() verzichtet werden kann:


def loop_oper_dict_good(d: dict) -> None:
    for k in d:
        print(f"Key: {k}")

Will man das Dictionary bearbeiten, sollte eine Kopie der Schlüsselworte erstellt werden, da sich die dem Dictionary zugrunde liegende Liste von Schlüsselworten ändern kann, wenn neue Werte hinzugefügt oder welche gelöscht werden. Aber auch hier kann man auf das explizite Abrufen der Schlüsselworte mittels keys() verzichten.


def loop_oper_dict_better(d: dict) -> None:
    for k in list(d): # not list(d.keys())
        print(f"Key: {k}")

Schleife zum Nutzen der Werte eines Dictionary

Will man in der Schleife auch auf die Werte des Dictionary zugreifen, könnte man annehmen, dass man in der Schleife die Werte explizit mit dem derzeitigen Schlüsselwort auslesen muss:


def loop_oper_dict_bad(d: dict) -> None:
    for k in d:
        print(f"{k} = {d[k]}")

Allerdings stellt hierfür Python die Funktion items() bereit, mit der sowohl die Schlüsselworte, als auch der dazugehörige Wert an die Schleife übergeben werden:


def loop_oper_dict_good(d: dict) -> None:
    for k, v in d.items():
        print(f"{k} = {v}")

Entpacken eines Tuple

Tuples können wie Listen durch Indexe ausgelesen werden.


def tuple_unpacking_bad() -> None:
    t = 4.33, 3.44
    x = t[0]
    y = t[2]

Weiß man jedoch, wie viele Felder das Tuple aufweist, kann man in einer Zeile alle Variablen angeben, auf die die Werte der Felder verteilt werden sollen:


def tuple_unpacking_good() -> None:
    t = 4.33, 3.44
    x, y = t

Dieselbe Vorgehensweise funktioniert auch bei Funktionen, die ein Tuple zurückgeben.

Schleife mit Zähler

Aus anderen Programmiersprache kennt man vielleicht, dass ein externe Zähler (z.B. i) definiert wird, der dann innerhalb der Schleife erhöht wird.


def index_in_loop_bad() -> None:
    l = [3.44, 8.14, 5.0]
    i = 0
    for x in l:
        print(f"@{i} = {x}")
        i += 1

Wird der Zähler aber immer nur um ein erhöht (also mit i += 1), kann man eine Zählerfunktion (bzw. Iterator) namens enumerate() benutzen:


def index_in_loop_good() -> None:
    l = [3.44, 8.14, 5.0]
    for i, x in enumerate(l):
        print(f"@{i} = {x}")

Laufzeitbestimmung

Manchmal mag man die Geschwindigkeit seines Codes prüfen und dafür die Zeit vor und nach der Codeausführung vergleichen:


import time

def timimg_bad() -> None:
    start = time.time()
    time.sleep(1) # Simulating code execution ...
    end = time.time()
    print(end - start)

Die Funktion time() gibt jedoch eine klassische Zeitangabe zurück, die u.U. ungenau sein kann. Für eine genauere Bestimmung der Ausführungsdauer vom Code, sollte jedoch die Ausgabe von perf_counter() benutzt werden. Mit dieser Funktion wird nämlich die Zeit als Gleitkommazahl mit höchstmöglicher Genauigkeit ausgegeben:


import time

def timimg_good() -> None:
    start = time.perf_counter()
    time.sleep(1) # Simulating code execution ...
    end = time.perf_counter()
    print(end - start)

Log statt Ausgaben benutzen

Um mitzuverfolgen, ob der Code richtig ausgeführt wird und um zu prüfen, an welcher Stelle sich die Ausführung des Codes befindet, kann man mit print() Zeichenketten an die Konsole ausgeben.


def execution_progress_bad() -> None:
    print("Start")
    print("Running")
    print("Something went wrong!")

Will man die Ausgabe dann (z.B. im fertigen Code) unterdrücken, muss man jede Stelle, an der die Funktion aufgerufen wird, löschen oder auskommentieren. Zudem bietet diese Art der Ablaufüberwachung keine Möglichkeit sie auf Fehler-, Status- oder Debugausgaben zu begrenzen. Desswegen sollte besser das Modul logging benutzt werden.


import logging

def execution_progress_good() -> None:
    logging.debug("Start")
    logging.info("Running")
    logging.error("Something went wrong!")

def main() -> None:
    level = logging.DEBUG
    format = "[%(levelname)s] %(asctime)s - %(message)s"
    logging.basicConfig(level = level, format = format)

Hiermit kann man sowohl die Stufe der Ausgabe als auch das Format der Ausgabe bestimmen.

Shell-Befehle

Um mit Programmen des Betriebssystems zu interagieren, ruft man manchmal Shell-Befehle auf.


import subprocess

def subprocess_call_bad() -> None:
    subprocess.run(
        ["ls -l"],
        capture_output = True,
        shell = True
    )

Hierbei sollte man jedoch darauf verzichten, das shell auf True zu setzen, da dies zu Sicherheitslücken führen kann. Zudem sollten die übergebenen Befehle separat in der Liste übergeben werden. Es ist gut möglich, dass man das shell-Argument setzt, grade weil man Liste nicht richtig erstellt.


import subprocess

def subprocess_call_good() -> None:
    subprocess.run(
        ["ls", "-l"],
        capture_output = True
    )

Mathematik mit Python

Um komplexere mathematische Rechnungen (insbesondere Matrizen- oder Vektorrechnungen) anzustellen, kann man natürlich Listen benutzen:


def add_vectors_bad() -> list:
    a = list(range(1000))
    b = list(range(1000))
    return [i + j for i, j in zip(a, b)]

Allerdings wird hierdurch der Code schlechter lesbar und es wird schwieriger, neben sonstigen Programmierfehlern auch die Fehler zu entdecken, die beim Übersetzen der mathematischen Formel in Code passiert sind. Daher ist es empfehlenswert die Bibliothek Numpy zu benutzen:


import numpy as np
def add_vectors_good() -> np.ndarray:
    a = np.arange(1000)
    b = np.arange(1000)
    return a + b

Für allgemeinere Datenanalyse sollte zudem die Bibliothek Pandas benutzt werden.

Importieren

Es besteht zwar die Möglichkeit, alle Funktionen, Untermodule und Klassen eines Moduls mittels * zu importieren:


from itertools import *

Allerdings wird dadurch der Namensraum vollgemüllt. Stattdessen sollte lieber nur die wirklich benutzten Funktionen usw. importiert werden:


from itertools import count

Importieren eigener Dateien

In eigenen Projekten werden Abhängigkeiten oft unmittelbar importiert, da sie in einer flachen Ordnerstruktur in unmittelbarer Nähe zum Hauptcode gespeichert sind.


from my_module import my_func

Es ist jedoch empfehlenswert, Module in Pakete (bzw. Packages) zu verpacken und gezielter zu importieren. Zwar ändert das nichts am Code selbst, jedoch wird die Projektstruktur organisierter und übersichtlicher.


from my_package.my_module import my_func

PEP 8

Zum Schluss noch was zum Coding-Stil. PEP 8 ist nämlich eine reine Stilrichtlinie und dessen Umsetzung hat keinen Einfluss auf den Code selbst.


def pep8_bad() -> None:
    x = (1,2)
    y=6.44
    l = [1,6,4]
    
    def my_func(i: AnyStr=None, limit = 100) -> None:
        pass

Grade für die Arbeit in größeren Entwicklerteams, sollte man sich einen einheitlichen Stil aneignen. Und wenn es schon einen standardisierten Stil gibt, warum nicht gleich den?


def pep8_good() -> None:
    x = (1, 2)
    y = 6.44
    l = [1, 6, 4]
    
    def my_func(i: AnyStr = None, limit=100) -> None:
        pass