Monkey-Patching in Python

Jay Shaw 21 Juni 2023
  1. Bedeutung dynamischer Sprachen beim Monkey-Patching
  2. Implementieren Sie einen Monkey-Patch in Python
  3. Verwenden Sie Monkey Patch für Unit-Tests in Python
  4. Abschluss
Monkey-Patching in Python

Ein Stück Code wird geschrieben, um das gewünschte Ergebnis zu erzielen, z. B. das Senden von Daten vom Benutzer an eine Datenbank. Aber der Code muss während der Testphasen optimiert werden, z. B. um zu überprüfen, ob der Code korrekt läuft oder ob Fehler vorhanden sind.

Beim Monkey-Patching wird ein Stub oder ein ähnlicher Codeabschnitt zugewiesen, sodass das Standardverhalten des Codes geändert wird. Dieser Artikel konzentriert sich auf verschiedene Möglichkeiten zum Patchen von Affen in Python.

Bedeutung dynamischer Sprachen beim Monkey-Patching

Nur dynamische Sprachen, für die Python ein hervorragendes Beispiel ist, können für Monkey Patching verwendet werden. In statischen Sprachen, in denen alles definiert werden muss, ist Monkey Patching unmöglich.

Monkey Patching ist beispielsweise die Praxis, Attribute (ob Methoden oder Variablen) während der Laufzeit hinzuzufügen, anstatt die Objektbeschreibung zu ändern. Diese werden häufig verwendet, wenn mit solchen Modulen gearbeitet wird, deren Quellcode nicht verfügbar ist, was es schwierig macht, die Objektdefinitionen zu aktualisieren.

Monkey-Patching in Python kann hilfreich sein, wenn eine neue Version eines Objekts mit gepatchten Membern innerhalb eines Dekorators erstellt wird, anstatt ein vorhandenes Objekt oder eine vorhandene Klasse zu ändern.

Implementieren Sie einen Monkey-Patch in Python

Monkey Patching in Python wird durch dieses Programm demonstriert. Eine Methode wird einer neu dekorierten Methode zugewiesen, um sie während der Laufzeit zu patchen.

Code:

import pandas as pd


def word_counter(self):
    """This method will return all the words inside the column that has the word 'tom'"""
    return [i for i in self.columns if "tom" in i]


pd.DataFrame.word_counter_patch = word_counter  # monkey-patch the DataFrame class
df = pd.DataFrame([list(range(4))], columns=["Arm", "tomorrow", "phantom", "tommy"])
print(df.word_counter_patch())

Ausgang:

"C:\Users\Win 10\main.py"
['tomorrow', 'phantom', 'tommy']

Process finished with exit code 0

Lassen Sie uns den Code aufschlüsseln, um das Monkey-Patching in Python zu verstehen.

Die erste Codezeile importiert die Bibliothek pandas, die zum Erstellen von Datenrahmen im Programm verwendet wird.

import pandas as pd

Da die Unterscheidung zwischen einer Funktion und einer ungebundenen Methode in Python 3 weitgehend nutzlos ist, wird dann eine Methodendefinition erstellt, die ungebunden und frei außerhalb des Geltungsbereichs von Klassendefinitionen existiert:

def word_counter(self):
    """This method will return all the words inside the column that has the word 'tom'"""
    return [i for i in self.columns if "tom" in i]

Eine neue Klasse wird mit pd.Dataframe.word_counter erstellt. Dann wird die neu erstellte Klasse an die Methode word_counter angehängt.

Was es tut, ist, dass Monkey die Methode word_counter mit der Datenrahmenklasse patcht.

pd.DataFrame.word_counter_patch = word_counter  # monkey-patch the DataFrame class

Sobald die Methode an die Klasse angehängt ist, muss ein neuer Datenrahmen erstellt werden, um die Wörter zu speichern. Diesem Datenrahmen wird eine Objektvariable df zugeordnet.

df = pd.DataFrame([list(range(4))], columns=["Arm", "tomorrow", "phantom", "tommy"])

Zuletzt wird die Monkey-Patch-Klasse aufgerufen, indem ihr der Datenrahmen df übergeben wird, der gedruckt wird. Was hier passiert, ist, dass wenn der Compiler die Klasse word_counter_patch aufruft, das Monkey-Patching den Datenrahmen an die Methode word_counter übergibt.

Da Klassen und Methoden in dynamischen Programmiersprachen als Objektvariablen behandelt werden können, kann Monkey Patching in Python auf Methoden angewendet werden, die andere Klassen verwenden.

print(df.word_counter_patch())

Verwenden Sie Monkey Patch für Unit-Tests in Python

Bisher haben wir gelernt, wie Affen-Patching in Python auf Funktionen ausgeführt wird. In diesem Abschnitt wird untersucht, wie man die globalen Variablen mit Python patcht.

Zur Veranschaulichung dieses Beispiels werden Pipelines verwendet. Für Leser, die neu in Pipelines sind, ist es ein Prozess zum Trainieren und Testen von Modellen für maschinelles Lernen.

Eine Pipeline besteht aus zwei Modulen, einem Trainingsmodul, das Daten wie Text oder Bilder sammelt, und einem Testmodul.

Was dieses Programm tut, ist, dass die Pipeline erstellt wird, um nach mehreren Dateien im Datenverzeichnis zu suchen. In der Datei test.py erstellt das Programm ein temporäres Verzeichnis mit einer einzigen Datei und sucht nach der Anzahl von Dateien in diesem Verzeichnis.

Trainieren Sie die Pipeline für Unit-Tests

Das Programm erstellt eine Pipeline, die Daten aus zwei einfachen Textdateien sammelt, die in einem Verzeichnis namens data gespeichert sind. Um diesen Prozess nachzubilden, müssen wir die Python-Dateien pipeline.py und test.py in einem übergeordneten Verzeichnis erstellen, in dem der Ordner data gespeichert ist.

Die pipeline.py-Datei:

from pathlib import Path

DATA_DIR = Path(__file__).parent / "data"


def collect_files(pattern):
    return list(DATA_DIR.glob(pattern))

Lassen Sie uns den Code aufschlüsseln:

Die pathlib wird importiert, da Path im Code verwendet wird.

from pathlib import Path

Dies ist eine globale Variable DATA_DIR, die den Speicherort von Datendateien speichert. Der Pfad gibt die Datei innerhalb der übergeordneten Verzeichnisdaten an.

DATA_DIR = Path(__file__).parent / "data"

Eine Funktion collect_files wird erstellt, die einen Parameter übernimmt, nämlich das zu durchsuchende Zeichenfolgenmuster.

Die Methode DATA_DIR.glob sucht das Muster innerhalb des Datenverzeichnisses. Die Methode gibt eine Liste zurück.

def collect_files(pattern):
    return list(DATA_DIR.glob(pattern))

Wie kann die Methode collect_files korrekt getestet werden, da an dieser Stelle eine globale Variable verwendet wird?

Eine neue Datei, test.py, muss erstellt werden, um den Code zum Testen der Pipeline-Klasse zu speichern.

Die test.py-Datei:

import pipeline


def test_collect_files(tmp_path):
    # given
    temp_data_directory = tmp_path / "data"
    temp_data_directory.mkdir(parents=True)
    temp_file = temp_data_directory / "file1.txt"
    temp_file.touch()

    expected_length = 1

    # when
    files = pipeline.collect_files("*.txt")
    actual_length = len(files)

    # then
    assert expected_length == actual_length

Die erste Codezeile importiert die Python-Bibliotheken pipeline und pytest. Als nächstes wird eine test-Funktion namens test_collect_files erstellt.

Diese Funktion hat einen Parameter temp_path, der verwendet wird, um ein temporäres Verzeichnis zu erhalten.

def test_collect_files(tmp_path):

Die Pipeline ist in drei Abschnitte unterteilt: gegeben, wann und dann.

Innerhalb des Given wird eine neue Variable namens temp_data_directory erstellt, die nichts anderes als ein temporärer Pfad ist, der auf das data-Verzeichnis zeigt. Dies ist möglich, weil das Fixture tmp_path ein Pfadobjekt zurückgibt.

Als nächstes muss das Datenverzeichnis erstellt werden. Dies geschieht mit der Funktion mkdir, und das übergeordnete Verzeichnis wird auf true gesetzt, um sicherzustellen, dass alle übergeordneten Verzeichnisse innerhalb dieses Pfads erstellt werden.

Als nächstes wird in diesem Verzeichnis eine einzelne Textdatei mit dem Namen file1.txt erstellt und dann mit der touch-Methode erstellt.

Eine neue Variable, expected_length, wird erstellt, die die Anzahl der Dateien im Datenverzeichnis zurückgibt. Es erhält den Wert 1, da nur eine Datei im Datenverzeichnis erwartet wird.

temp_data_directory = tmp_path / "data"
temp_data_directory.mkdir(parents=True)
temp_file = temp_data_directory / "file1.txt"
temp_file.touch()

expected_length = 1

Jetzt tritt das Programm in den When-Abschnitt ein.

Wenn die Funktion pipeline.collect_files aufgerufen wird, gibt sie eine Liste von Dateien mit einem Muster *.txt zurück, wobei * eine Zeichenfolge ist. Es wird dann einer Variablen files zugewiesen.

Die Anzahl der Dateien wird mit len(files) abgerufen, das die Länge der Liste zurückgibt und in der Variablen actual_length gespeichert wird.

files = pipeline.collect_files("*.txt")
actual_length = len(files)

Im Then-Abschnitt gibt eine assert-Anweisung an, dass expected_length gleich actual_length sein muss. assert wird verwendet, um zu überprüfen, ob eine gegebene Aussage wahr ist.

Jetzt ist die Pipeline zum Testen bereit. Gehen Sie zum Terminal und führen Sie die Datei test.py mit dem folgenden Befehl aus:

pytest test.py

Wenn der Test ausgeführt wird, schlägt er fehl.

 assert expected_length == actual_length
E       assert 1 == 0

test.py:23: AssertionError
=============================== short test summary info ============================================
FAILED test.py::test_collect_files - assert 1 == 0

Dies geschieht, weil die erwartete Länge 1 ist, aber in Wirklichkeit 2 ist. Dies geschieht, weil das Programm zu diesem Zeitpunkt das Temp-Verzeichnis nicht verwendet; stattdessen verwendet es das echte Datenverzeichnis, das zu Beginn des Programms erstellt wurde.

Im Datenverzeichnis wurden zwei Dateien erstellt, während im temporären Verzeichnis nur eine einzige Datei erstellt wurde. Was passiert ist, dass der Code test.py geschrieben wird, um nach Dateien im temporären Verzeichnis zu suchen, in dem nur eine einzige Datei gespeichert ist, aber stattdessen bewirkt der Code, dass er in das ursprüngliche Verzeichnis zurückkehrt.

Deshalb erhält die Variable expected_length den Wert 1, aber beim Vergleich mit actual_length schlägt der Test fehl.

Wir können die globale Variable patchen, um dieses Problem mit einem Monkey-Patch zu lösen.

Als erstes muss der Funktion collect_files ein Parameter monkeypatch wie folgt hinzugefügt werden:

def test_collect_files(tmp_path, monkeypatch):

Was jetzt getan werden muss, ist, dass die globale Variable mit dem Monkey-Patch gepatcht wird:

def test_collect_files(tmp_path, monkeypatch):
    # given
    temp_data_directory = tmp_path / "data"
    temp_data_directory.mkdir(parents=True)
    temp_file = temp_data_directory / "file1.txt"
    temp_file.touch()

    monkeypatch.setattr(pipeline, "DATA_DIR", temp_data_directory)  # Monkey Patch

    expected_length = 1

Monkey Patching in Python hat eine Funktion setattr, die es ermöglicht, der Variablen DATA_DIR innerhalb des Pipeline-Moduls einen neuen Wert zuzuweisen. Und der neue Wert für DATA_DIR wird temp_data_directory zugewiesen.

Wenn der Test erneut ausgeführt wird, ist er bestanden, da die globale Variable gepatcht ist und stattdessen temp_data_directory verwendet.

platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: C:\Users\Win 10\PycharmProjects\Monkey_Patch
collected 1 item

test.py .                                                                                                                                                      [100%]

================================== 1 passed in 0.02s ====================================

Abschluss

Dieser Artikel konzentriert sich auf das Monkey-Patching in Python und erklärt detailliert die praktische Verwendung von Monkey-Patching. Der Leser wird in der Lage sein, Monkey Patching einfach zu implementieren.

Verwandter Artikel - Python Unittest