Estoy en Python 3.5.1. Escribí una biblioteca, y bajo ciertas circunstancias (cuando el archivo se ejecuta directamente, es decir, __name__ == '__main__') quiero decorar ciertos métodos en una de las clases. Debe decorar todas las instancias que se puedan crear. Me gustaría hacerlo de una manera que no sea invasiva, es decir, idealmente la clase en mi biblioteca no necesitaría ningún código especial.

Después de un tiempo, logré implementar algo como esto, que cumple con mis requisitos:

def patch(clazz, name, replacement):

    def wrap_original(orig):
        # when called with the original function, a new function will be returned
        # this new function, the wrapper, replaces the original function in the class
        # and when called it will call the provided replacement function with the
        # original function as first argument and the remaining arguments filled in by Python

        def wrapper(*args, **kwargs):
            return replacement(orig, *args, **kwargs)

        return wrapper

    orig = getattr(clazz, name)
    setattr(clazz, name, wrap_original(orig))

def replacement_function(orig, self, ... other argumnents ...):
    # orig here is the original function, so can be called like this:
    # orig(self, ... args ...)
    pass

patch(mylib.MyClass, 'method_name', replacemment_function)

Sorprendentemente, este código funciona, aunque todavía no lo he probado con métodos de clase, pero no lo necesito ahora. También parchea instancias creadas antes del parcheo, aunque todavía no estoy seguro de si es bueno o no; d

El código anterior es posiblemente difícil, necesité un tiempo para entender cómo funciona después de escribirlo, para poder escribir el comentario explicativo. Me encantaría algo más fácil.

La pregunta: ¿hay algo en la biblioteca de Python que haría innecesario un código como este, que ya implementa lo que estoy haciendo, pero mejor?

3
wujek 8 may. 2016 a las 21:40

4 respuestas

La mejor respuesta

Uno de los carteles aquí, que lamentablemente borró su publicación, me dirigió hacia el módulo functools. Al final, me decidí por lo siguiente:

def replacement(self, orig, ... other arguments ...):
    # orig here is the original function, so can be called like this:
    # orig(self, ... args ...)
    pass

mylib.MyClass.my_method = functools.partialmethod(replacement, mylib.MyClass.my_method)

Los argumentos orig y self necesarios para cambiar de lugar, como resultado de partialmethod enlazan el primer argumento a la instancia en la que se encuentra, y el segundo en este caso será la función original ( el segundo argumento para partialmethod). Se ve mucho más limpio.

0
wujek 12 may. 2016 a las 20:43

Su enfoque parece ser la forma más pitónica de hacerlo.

Gevent, una biblioteca popular que utiliza parches de mono, realiza mono parcheado casi de la misma manera que usted describe.

1
Rushy Panchal 8 may. 2016 a las 18:46

Otra alternativa sería crear una función decoradora "nula", y luego cambiar entre esa función y el decorador "real" utilizando su lógica condicional:

from decorator_lib import real_decorator

def fake_decorator(fun):
    return fun

if __name__ == '__main__':
    my_decorator = real_decorator
else:
    my_decorator = fake_decorator


# ... elsewhere in the module ...

@my_decorator
def method(self, a, b, c):
    pass

# ... finally:
if __name__ == '__main__':
    run_self_tests_or_whatever()
0
aghast 8 may. 2016 a las 22:38

Los métodos se crean dinámicamente cuando se buscan en instancias; las instancias no tienen copias de todos los métodos, sino que el protocolo descriptor toma funciones del clase y los vincula a la instancia según sea necesario. Es por eso que monkeypatching la clase funciona aquí; instance.method_name encontrará mylib.MyClass.method_name cuando se realiza la búsqueda de atributos .

No hay nada en la biblioteca predeterminada que realice lo que está haciendo aquí, no, porque un código diferente puede necesitar diferentes patrones de manejo de delegación de vuelta al método anterior.

Su enfoque se parece mucho a cómo el proyecto Mercurial admite el ajuste de funciones , ya que el original se pasa al contenedor.

3
Martijn Pieters 8 may. 2016 a las 18:51