Aplicación de parches de mono en Python

Jay Shaw 21 junio 2023
  1. Importancia de los lenguajes dinámicos en Monkey Patching
  2. Implementar un Monkey Patch en Python
  3. Use Monkey Patch para pruebas unitarias en Python
  4. Conclusión
Aplicación de parches de mono en Python

Se escribe un fragmento de código para lograr el resultado deseado, como enviar datos del usuario a una base de datos. Pero el código debe modificarse durante las fases de prueba, como verificar si el código se ejecuta correctamente o si hay errores.

Monkey patching es el proceso de asignar un stub o una pieza de código similar para que se cambie el comportamiento predeterminado del código. Este artículo se centrará en las diferentes formas de parchear a los monos en python.

Importancia de los lenguajes dinámicos en Monkey Patching

Solo los lenguajes dinámicos, de los cuales Python es un excelente ejemplo, pueden usarse para parchear monos. En lenguajes estáticos donde todo debe definirse, la aplicación de parches mono es imposible.

Como ejemplo, la aplicación de parches mono es la práctica de agregar atributos (ya sean métodos o variables) durante el tiempo de ejecución en lugar de alterar la descripción del objeto. Estos se utilizan con frecuencia cuando se trabaja con aquellos módulos cuyo código fuente no está disponible, lo que dificulta la actualización de las definiciones de objetos.

Los parches mono en Python pueden ser útiles si se crea una nueva versión de un objeto con miembros parcheados dentro de un decorador en lugar de modificar un objeto o clase existente.

Implementar un Monkey Patch en Python

La aplicación de parches de mono en Python se demostrará a través de este programa. Se asignará un método a un nuevo método decorado para parchearlo durante el tiempo de ejecución.

Código:

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())

Producción :

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

Process finished with exit code 0

Desglosemos el código para comprender la aplicación de parches mono en Python.

La primera línea de código importa la biblioteca pandas utilizada para crear marcos de datos en el programa.

import pandas as pd

Luego, dado que la distinción entre una función y un método independiente es en gran parte inútil en Python 3, se establece una definición de método que existe sin restricciones y libre fuera del alcance de cualquier definición de clase:

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]

Se crea una nueva clase usando pd.Dataframe.word_counter. Luego, la clase recién creada se adjunta al método word_counter.

Lo que hace es parchear el método word_counter con la clase de marco de datos.

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

Una vez que el método se adjunta a la clase, se debe crear un nuevo marco de datos para almacenar las palabras. A este marco de datos se le asigna una variable de objeto df.

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

Por último, se llama a la clase monkey patch pasándole el marco de datos df, que se imprime. Lo que sucede aquí es que cuando el compilador llama a la clase word_counter_patch, el monkey patching pasa la trama de datos al método word_counter.

Como las clases y los métodos se pueden tratar como variables de objeto en los lenguajes de programación dinámicos, el parche mono en Python se puede aplicar a los métodos que usan otras clases.

print(df.word_counter_patch())

Use Monkey Patch para pruebas unitarias en Python

Hasta ahora, hemos aprendido cómo se ejecutan los parches mono en Python en las funciones. Esta sección examinará cómo parchear las variables globales usando Python.

Se usarán canalizaciones para demostrar este ejemplo. Para los lectores nuevos en las canalizaciones, es un proceso para entrenar y probar modelos de aprendizaje automático.

Una canalización tiene dos módulos, un módulo de capacitación que recopila datos, como texto o imágenes, y un módulo de prueba.

Lo que hace este programa es que se crea el pipeline para buscar varios archivos en el directorio de datos. En el archivo test.py, el programa crea un directorio temporal con un solo archivo y busca la cantidad de archivos en ese directorio.

Capacitar la canalización para pruebas unitarias

El programa crea una tubería que recopila datos de dos archivos de texto sin formato almacenados dentro de un directorio, datos. Para recrear este proceso, debemos crear los archivos de Python pipeline.py y test.py en un directorio principal donde se almacena la carpeta data.

El archivo pipeline.py:

from pathlib import Path

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


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

Desglosemos el código:

El pathlib se importa como Path se utilizará dentro del código.

from pathlib import Path

Esta es una variable global DATA_DIR que almacena la ubicación de los archivos de datos. La Ruta indica el archivo dentro de los datos del directorio principal.

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

Se crea una función collect_files que toma un parámetro, que es el patrón de cadena que se necesita buscar.

El método DATA_DIR.glob busca el patrón dentro del directorio de datos. El método devuelve una lista.

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

¿Cómo se puede probar correctamente el método collect_files ya que se utiliza una variable global en este punto?

Se debe crear un nuevo archivo, test.py, para almacenar el código para probar la clase de canalización.

El archivo test.py:

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

La primera línea de código importa las bibliotecas de Python pipeline y pytest. A continuación, se crea una función de prueba llamada test_collect_files.

Esta función tiene un parámetro temp_path que se utilizará para obtener un directorio temporal.

def test_collect_files(tmp_path):

La canalización se divide en tres secciones: Dado, Cuándo y Entonces.

Dentro de Given, se crea una nueva variable llamada temp_data_directory, que no es más que una ruta temporal que apunta al directorio data. Esto es posible porque el dispositivo tmp_path devuelve un objeto de ruta.

A continuación, se debe crear el directorio de datos. Se realiza utilizando la función mkdir, y el directorio principal se establece en verdadero para garantizar que se creen todos los directorios principales dentro de esta ruta.

A continuación, se crea un único archivo de texto dentro de este directorio, llamado file1.txt, y luego se crea utilizando el método touch.

Se crea una nueva variable, longitud_esperada, que devuelve el número de archivos dentro del directorio de datos. Se le da un valor de 1 ya que solo se espera un archivo dentro del directorio de datos.

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

Ahora el programa entra en la sección Cuándo.

Cuando se invoca la función pipeline.collect_files, devuelve una lista de archivos que tienen un patrón *.txt, donde * es una cadena. Luego se asigna a una variable archivos.

El número de archivos se obtiene usando len(files), que devuelve la longitud de la lista y se almacena dentro de la variable actual_length.

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

En la sección Luego, una declaración de afirmación establece que longitud_esperada debe ser igual a longitud_real. afirmar se utiliza para comprobar si una afirmación dada es verdadera.

Ahora la canalización está lista para la prueba. Dirígete a la terminal y ejecuta el archivo test.py usando el comando:

pytest test.py

Cuando se ejecuta la prueba, falla.

 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

Sucede porque la longitud esperada es 1, pero en realidad es 2. Esto sucede porque, en este punto, el programa no está usando el directorio temporal; en su lugar, utiliza el directorio de datos reales creado al principio del programa.

Se crearon dos archivos dentro del directorio de datos, mientras que solo se creó un único archivo dentro del directorio temporal. Lo que sucede es que el código test.py se escribe para buscar archivos dentro del directorio temporal donde solo se almacena un archivo, pero en cambio, el código hace que regrese al directorio original.

Por eso a la variable longitud_esperada se le da el valor 1, pero cuando se compara con longitud_real, la prueba falla.

Podemos parchear la variable global para resolver este problema usando un parche de mono.

Al principio, se debe agregar un parámetro monkeypatch a la función collect_files como este:

def test_collect_files(tmp_path, monkeypatch):

Ahora, lo que debe hacerse ahora es que la variable global se parcheará usando el parche de mono:

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 en Python tiene una función setattr, que permite asignar un nuevo valor a la variable DATA_DIR dentro del módulo de canalización. Y el nuevo valor para DATA_DIR se asigna a temp_data_directory.

Si la prueba se ejecuta de nuevo, se pasa porque la variable global está parcheada y usa temp_data_directory en su lugar.

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 ====================================

Conclusión

Este artículo se centra en la aplicación de parches mono en Python y explica en detalle los usos prácticos de la aplicación de parches mono. El lector podrá implementar parches mono fácilmente.

Artículo relacionado - Python Unittest