Kategorie: pythonSchwierigkeit: MediumVeröffentlicht: 2024-12-18

Veränderbare Default-Argumente in Python

Einer der häufigsten Fallstricke in Python ist das Verhalten von veränderbaren (mutables) Default-Argumenten in Funktionsdefinitionen. Dies kann zu unerwartetem Verhalten und schwer zu findenden Bugs führen, besonders in größeren Codebasen.

Das Problem verstehen

In Python werden Default-Argumente zum Zeitpunkt der Funktionsdefinition ausgewertet und gespeichert - nicht bei jedem Funktionsaufruf. Dieses Verhalten, obwohl konsistent mit Pythons Ausführungsmodell, kann zu überraschenden Ergebnissen führen, wenn das Default-Argument ein veränderbares Objekt wie eine Liste, ein Dictionary oder ein Set ist.

Wichtige Konzepte:

  1. Zeitpunkt der Funktionsdefinition: Default-Argumente werden einmalig bei der Funktionsdefinition ausgewertet.
  2. Objektreferenz: Dasselbe Objekt wird über alle Funktionsaufrufe hinweg wiederverwendet.
  3. Veränderbarkeit: Änderungen am Objekt bleiben zwischen Funktionsaufrufen bestehen.

Häufige Szenarien

Hier sind typische Situationen, in denen dieses Problem auftritt:

# Szenario 1: Listen-Akkumulation def nutzer_hinzufuegen(name, nutzer=[]): nutzer.append(name) return nutzer print(nutzer_hinzufuegen("Alice")) # Ausgabe: ["Alice"] print(nutzer_hinzufuegen("Bob")) # Ausgabe: ["Alice", "Bob"] - Unerwartet! # Szenario 2: Dictionary-Caching def daten_cachen(schluessel, wert, cache={}): cache[schluessel] = wert return cache print(daten_cachen("a", 1)) # Ausgabe: {"a": 1} print(daten_cachen("b", 2)) # Ausgabe: {"a": 1, "b": 2} - Cache bleibt erhalten! # Szenario 3: Klassen-Attribut-Initialisierung class Benutzer: def __init__(self, items=[]): self.items = items benutzer1 = Benutzer() benutzer1.items.append("buch") benutzer2 = Benutzer() # benutzer2.items enthält bereits "buch"!

Wie du das Problem erkennst

  1. Code-Review-Muster:

    • Funktionen mit veränderbaren Default-Argumenten (Listen, Dictionaries, Sets)
    • Klassenattribute, die mit leeren Sammlungen initialisiert werden
    • Caching- oder Akkumulator-Funktionen
  2. Laufzeit-Symptome:

    • Unerwartete Datenansammlung
    • Gemeinsamer Zustand zwischen Funktionsaufrufen
    • Objekte, die Daten von vorherigen Operationen enthalten
  3. Test-Probleme:

    • Tests laufen einzeln erfolgreich, schlagen aber zusammen fehl
    • Reihenfolgenabhängige Testfehler
    • Inkonsistentes Verhalten in lang laufenden Anwendungen

Die Lösung

Hier sind verschiedene Möglichkeiten, dieses Problem zu beheben:

# Lösung 1: None als Default verwenden def nutzer_hinzufuegen(name, nutzer=None): if nutzer is None: nutzer = [] nutzer.append(name) return nutzer # Lösung 2: Unveränderlichen Default + Konvertierung verwenden def items_verarbeiten(items=()): # Tuple als Default items_liste = list(items) # items_liste verarbeiten... return items_liste # Lösung 3: Factory-Funktions-Muster def neue_liste_erstellen(): return [] def nutzer_hinzufuegen(name, nutzer=None): nutzer = neue_liste_erstellen() if nutzer is None else nutzer nutzer.append(name) return nutzer

Praxisbeispiel

Hier ist ein komplexeres Praxisbeispiel mit einem Cache-Dekorator:

# Problematische Implementierung def ergebnisse_cachen(func, cache={}): def wrapper(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrapper # Bessere Implementierung def ergebnisse_cachen(func): def wrapper(cache=None, *args): if cache is None: cache = {} if args not in cache: cache[args] = func(*args) return cache[args] return wrapper # Beste Implementierung mit functools from functools import lru_cache @lru_cache(maxsize=None) def aufwendige_funktion(n): # Komplexe Berechnung hier return n ** 2

Vorbeugende Maßnahmen

  1. Code-Review-Richtlinien:

    • Achte auf veränderbare Default-Argumente in Funktionsdefinitionen
    • Überprüfe Klassenattribut-Initialisierungen
    • Prüfe Caching-Implementierungen
  2. Verwende statische Analyse-Tools:

    # pylint-Konfiguration [BASIC] disable=dangerous-default-value
  3. Test-Strategien:

    def test_nutzer_hinzufuegen(): # Test-Isolation ergebnis1 = nutzer_hinzufuegen("Alice") assert ergebnis1 == ["Alice"] # Test neue Instanz ergebnis2 = nutzer_hinzufuegen("Bob") assert ergebnis2 == ["Bob"] # Test explizite Liste eigene_liste = ["Charlie"] ergebnis3 = nutzer_hinzufuegen("David", eigene_liste) assert ergebnis3 == ["Charlie", "David"]

Best Practices

  1. Verwende unveränderliche Defaults:

    • None für optionale veränderbare Argumente
    • Tuples statt Listen wo möglich
    • Frozen sets statt normaler Sets
  2. Factory-Funktionen:

    • Erstelle neue Instanzen bei Bedarf
    • Implementiere klare Objekt-Erstellungsmuster
    • Dokumentiere das Verhalten von Factory-Funktionen
  3. Dokumentation:

    def daten_verarbeiten(items=None): """Verarbeitet eine Liste von Items. Args: items: Optionale Liste von zu verarbeitenden Items. Wenn None, wird eine neue Liste erstellt. Hinweis: Diese Funktion erstellt eine neue Liste wenn items None ist, um Probleme mit veränderbaren Default-Argumenten zu vermeiden. """ items = [] if items is None else items # Items verarbeiten... return items

Häufige Fehler, die du vermeiden solltest

  1. Mehrere veränderbare Defaults:

    # Falsch def verarbeiten(items=[], cache={}, gesehen=set()): # Das ist problematisch! pass # Richtig def verarbeiten(items=None, cache=None, gesehen=None): items = [] if items is None else items cache = {} if cache is None else cache gesehen = set() if gesehen is None else gesehen
  2. Verschachtelte veränderbare Strukturen:

    # Falsch def verschachtelte_defaults(daten={'nutzer': []}): # Verschachtelte veränderbare Strukturen sind noch problematischer pass # Richtig def verschachtelte_defaults(daten=None): if daten is None: daten = {'nutzer': []}

Denk dran: Auch wenn Pythons veränderbare Default-Argumente tückisch sein können, hilft das Verständnis des zugrundeliegenden Mechanismus dabei, zuverlässigeren Code zu schreiben. Bevorzuge immer explizite Initialisierung gegenüber impliziter Zustandsverwaltung.

Selbst ausprobieren

Verbleibende Korrekturen: 10