HOWTO · Python

Monkey-Patching in Python

Dieser Artikel erklärt, was Monkey Patching ist und wie es in Python implementiert werden kann. Es hat zwei Abschnitte; Die erste demonstriert Monkey Patching in Funktionen und die zweite zeigt, wie es beim Komponententesten von Dateien implementiert werden kann.

Auf dieser Seite

Monkey-Patching ändert oder ergänzt ein Attribut, während ein Programm läuft. In Python beeinflusst eine einer Klasse zugewiesene Funktion oder ein ersetzter Modulname, was spätere Attributzugriffe finden. Das ist für gezielte Tests nützlich, kann das Verhalten aber davon abhängig machen, wann und an welcher Stelle die Änderung erfolgt.

Was Monkey-Patching in Python bedeutet

Python ermöglicht es Programmen, vielen Objekten zur Laufzeit Attribute zuzuweisen. Monkey-Patching bedeutet üblicherweise, ein vorhandenes Attribut zu ändern oder ein neues hinzuzufügen, damit der Code ein Ersatzverhalten verwendet. Dafür muss die ursprüngliche Quelldatei nicht geändert werden. Die Technik ist auch nicht dynamischen Sprachen vorbehalten: Andere Sprachen bieten über ihre Laufzeitumgebung oder Testwerkzeuge vergleichbare Möglichkeiten.

Die beiden Beispiele in diesem Leitfaden hängen zusammen, unterscheiden sich aber. Wird einer Klasse eine Funktion zugewiesen, erhalten Instanzen dieser Klasse zusätzliches Verhalten. Wird eine globale Variable in einem Modul ersetzt, ändert sich der Wert, den eine Funktion dieses Moduls bei ihrem Aufruf ausliest. Das zweite Muster ist in Tests oft hilfreich, wenn der Produktionscode einen konfigurierten Pfad oder eine andere Abhängigkeit verwendet.

Einer Klasse zur Laufzeit eine Methode hinzufügen

Wird eine gewöhnliche Funktion einer Klasse zugewiesen, bindet Pythons Deskriptorverhalten die Instanz als erstes Argument, wenn die Funktion über diese Instanz abgerufen wird. Definieren Sie die Funktion mit self und weisen Sie sie anschließend der Klasse zu. Dadurch wird das vorhandene Klassenobjekt geändert; es entsteht keine neue Klasse.

Bei der Zuweisung einer Funktion an eine Klasse wird sie als Deskriptor gespeichert. Beim Zugriff über eine Instanz wird diese als erstes Argument gebunden. Definieren Sie daher einen Parameter namens self. Führen Sie folgendes Beispiel aus:

Der Code und die angezeigten Bezeichnungen oder Ausgaben in diesem Abschnitt bleiben auf Englisch.

class Greeter:
    pass


def greet(self, name):
    return f"Hello, {name}!"


Greeter.greet = greet
print(Greeter().greet("Ada"))
Hello, Ada!

Die Zuweisung wirkt sich im aktuellen Prozess auf Greeter und dessen Instanzen aus. Wird eine Funktion stattdessen nur einer einzelnen Instanz zugewiesen, wird sie an dieser Instanz gespeichert; sie erhält nicht automatisch dieselbe Methodenbindung wie eine Funktion, die über die Klasse gefunden wird. Weisen Sie die Funktion der Klasse zu, wenn alle Instanzen die neue Methode erhalten sollen.

Eine globale Modulvariable in einem pytest-Test ersetzen

Angenommen, pipeline.py definiert DATA_DIR = Path(__file__).parent / "data", und die Funktion collect_files(pattern) gibt list(DATA_DIR.glob(pattern)) zurück. Beim Aufruf liest die Funktion DATA_DIR aus dem Modul pipeline. Ein temporäres Verzeichnis im Test ändert diesen Zugriff nicht von selbst. Der Test muss deshalb den Namen in dem Modul ersetzen, in dem die Funktion ihn nachschlägt.

In einem pytest-Test stellt tmp_path einen temporären Path bereit. Das folgende Beispiel definiert ein kleines pipeline-Modul, dessen Funktion collect_files DATA_DIR aus dem Namensraum dieses Moduls liest. Das reguläre Datenverzeichnis ist leer, während das Testverzeichnis file1.txt enthält. Vor dem Patch würde assert pipeline.collect_files("*.txt") == [expected_file] fehlschlagen: Der Vergleich lautet [] == [expected_file]. Nach monkeypatch.setattr gibt die Funktion die Datei aus dem Testverzeichnis zurück. Dies beschreibt den deterministischen Fehler für den gezeigten Aufbau; es handelt sich nicht um die Ausgabe eines tatsächlich ausgeführten pytest-Laufs.

Der Code und die angezeigten Bezeichnungen oder Ausgaben in diesem Abschnitt bleiben auf Englisch.

from pathlib import Path
import tempfile
import types


pipeline = types.ModuleType("pipeline")
production_data = tempfile.TemporaryDirectory()
pipeline.DATA_DIR = Path(production_data.name)
exec(
    "def collect_files(pattern):\n"
    "    return list(DATA_DIR.glob(pattern))\n",
    pipeline.__dict__,
)


def test_collect_files_uses_temporary_data(tmp_path, monkeypatch):
    test_data = tmp_path / "data"
    test_data.mkdir()
    expected_file = test_data / "file1.txt"
    expected_file.write_text("sample", encoding="utf-8")

    files_before_patch = pipeline.collect_files("*.txt")
    assert files_before_patch == []

    monkeypatch.setattr(pipeline, "DATA_DIR", test_data)
    assert pipeline.collect_files("*.txt") == [expected_file]

Das Ziel des Patches ist entscheidend: Ersetzen Sie pipeline.DATA_DIR, weil collect_files diese globale Variable im Modul nachschlägt. Eine gleichnamige Variable im Testmodul zu ersetzen, ändert nicht die globale Variable, die pipeline.collect_files verwendet. Der Name der pytest-Fixture lautet monkeypatch; mit monkeypatch.setattr wird das Attribut ersetzt. pytest stellt den ursprünglichen Wert nach dem Test wieder her, auch wenn der Test fehlschlägt. So bleibt die Änderung auf diesen Test beschränkt. Die Fixture verwaltet den Ersatzwert; sie macht die temporären Testdateien oder das Verzeichnis nicht automatisch zum Datenverzeichnis des Moduls.

Monkey-Patching mit Bedacht einsetzen

Monkey-Patching ist nützlich, wenn ein Test eine Abhängigkeit ersetzen oder eine Einstellung auf Modulebene umleiten muss, ohne den Aufrufpfad im Produktionscode zu ändern. Beschränken Sie den Ersatz auf den Test, ändern Sie den Namen, den der Code tatsächlich ausliest, und bevorzugen Sie die pytest-Fixture monkeypatch, damit das ursprüngliche Attribut nach dem Test wiederhergestellt wird.

Änderungen an gemeinsam genutzten Klassen oder Modulen können später ausgeführten, unabhängigen Code beeinflussen. Außerdem kann das Ergebnis von der Import- oder Testreihenfolge abhängen. Vermeiden Sie dauerhafte, anwendungsweite Patches, wenn sich eine Abhängigkeit durch gewöhnliche Konfiguration, Dependency Injection oder eine kleine Wrapper-Funktion klarer ausdrücken lässt. Ein Klassen-Patch eignet sich, wenn die hinzugefügte Methode für Instanzen dieser Klasse verfügbar sein soll. Ein Patch einer globalen Modulvariable eignet sich, wenn eine Funktion einen Modulnamen liest, den der Test ersetzen muss. Diese Unterscheidung erleichtert die Fehlersuche und vermeidet unerwartetes Verhalten.