¿Cómo un mono parcha una función en python?

Tengo problemas para reemplazar una función de un módulo diferente con otra función y me está volviendo loco.

Digamos que tengo un módulo bar.py que se ve así:

from a_package.baz import do_something_expensive def a_function(): print do_something_expensive() 

Y tengo otro módulo que se parece a esto:

 from bar import a_function a_function() from a_package.baz import do_something_expensive do_something_expensive = lambda: 'Something really cheap.' a_function() import a_package.baz a_package.baz.do_something_expensive = lambda: 'Something really cheap.' a_function() 

Espero obtener los resultados:

 Something expensive! Something really cheap. Something really cheap. 

Pero en cambio me sale esto:

 Something expensive! Something expensive! Something expensive! 

¿Qué estoy haciendo mal?

Puede ayudar pensar cómo funcionan los espacios de nombres de Python: son esencialmente diccionarios. Así que cuando haces esto:

 from a_package.baz import do_something_expensive do_something_expensive = lambda: 'Something really cheap.' 

piensa en esto, de esta manera:

 do_something_expensive = a_package.baz['do_something_expensive'] do_something_expensive = lambda: 'Something really cheap.' 

Esperamos que pueda comprender por qué esto no funciona 🙂 Una vez que importe un nombre a un espacio de nombres, el valor del nombre en el espacio de nombres que importó es irrelevante. Solo está modificando el valor de do_something_expensive en el espacio de nombres del módulo local, o en el espacio de nombres de a_package.baz, arriba. Pero debido a que la barra importa do_something_expensive directamente, en lugar de hacer referencia a ella desde el espacio de nombres del módulo, debe escribir en su espacio de nombres:

 import bar bar.do_something_expensive = lambda: 'Something really cheap.' 

Hay un decorador muy elegante para esto: Guido van Rossum: lista de desarrolladores de Python: expresiones idiomáticas .

También está el paquete dectools , que vi un PyCon 2010, que también se puede usar en este contexto, pero que en realidad podría ser de otra manera (parpadeo en el nivel declarativo del método … donde no esté)

Si solo desea parchearlo para su llamada y, de lo contrario, dejar el código original, puede usar https://docs.python.org/3/library/unittest.mock.html#patch (desde Python 3.3):

 with patch('a_package.baz.do_something_expensive', new=lambda: 'Something really cheap.'): print do_something_expensive() # prints 'Something really cheap.' print do_something_expensive() # prints 'Something expensive!' 

En el primer fragmento, hace que bar.do_something_expensive refiera al objeto de función al que se refiere a_package.baz.do_something_expensive en ese momento. Para realmente “monkeypatch”, necesitaría cambiar la función en sí (solo está cambiando a qué se refieren los nombres); Esto es posible, pero en realidad no quieres hacer eso.

En sus bashs de cambiar el comportamiento de una a_function , ha hecho dos cosas:

  1. En el primer bash, hace que do_something_expensive sea un nombre global en su módulo. Sin embargo, está llamando a_function , que no busca en su módulo para resolver nombres, por lo que aún se refiere a la misma función.

  2. En el segundo ejemplo, cambias a lo que se refiere a_package.baz.do_something_expensive , pero bar.do_something_expensive no está ligado mágicamente a él. Ese nombre aún se refiere al objeto de función que buscó cuando se inició.

El enfoque más simple pero lejos del ideal sería cambiar bar.py para decir

 import a_package.baz def a_function(): print a_package.baz.do_something_expensive() 

La solución correcta es probablemente una de dos cosas:

  • Redefine a_function para tomar una función como argumento y llamar a eso, en lugar de intentar colarse y cambiar la función para la que está codificado, o
  • Almacene la función que se utilizará en una instancia de una clase; Así es como hacemos estado mutable en Python.

El uso de elementos globales (esto es lo que es cambiar las cosas a nivel de módulo de otros módulos) es algo malo que conduce a un código incomprensible, confuso, no verificable y no escalable cuyo flujo es difícil de rastrear.

do_something_expensive de la función a_function() es solo una variable dentro del espacio de nombres del módulo que apunta a un objeto de función. Cuando redefine el módulo, lo está haciendo en un espacio de nombres diferente.