web-dev-qa-db-fra.com

Comment un singe patche-t-il une fonction en python?

J'ai du mal à remplacer une fonction d'un module différent par une autre fonction et ça me rend fou.

Disons que j'ai un module bar.py qui ressemble à ceci:

from a_package.baz import do_something_expensive

def a_function():
    print do_something_expensive()

Et j'ai un autre module qui ressemble à ceci:

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

Je m'attendrais à obtenir les résultats:

Something expensive!
Something really cheap.
Something really cheap.

Mais à la place, j'obtiens ceci:

Something expensive!
Something expensive!
Something expensive!

Qu'est-ce que je fais mal?

73
guidoism

Il peut être utile de réfléchir au fonctionnement des espaces de noms Python: ce sont essentiellement des dictionnaires. Donc, lorsque vous faites cela:

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

pensez-y comme ceci:

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

J'espère que vous comprendrez pourquoi cela ne fonctionne pas :-) Une fois que vous avez importé un nom dans un espace de noms, la valeur du nom dans l'espace de noms que vous avez importé de n'a plus d'importance. Vous modifiez uniquement la valeur de do_something_expensive dans l'espace de noms du module local ou dans l'espace de noms de a_package.baz ci-dessus. Mais comme la barre importe directement do_something_expensive, plutôt que de la référencer depuis l'espace de noms du module, vous devez écrire dans son espace de noms:

import bar
bar.do_something_expensive = lambda: 'Something really cheap.'
83
Nicholas Riley

Il y a un décorateur vraiment élégant pour cela: Guido van Rossum: Liste Python-Dev: Idiomes Monkeypatching .

Il y a aussi le paquet dectools , que j'ai vu un PyCon 2010, qui peut également être utilisé dans ce contexte, mais qui pourrait en fait aller dans l'autre sens (monkeypatching au niveau déclaratif de la méthode ... où vous n'êtes pas)

21
RyanWilcox

Si vous souhaitez uniquement le patcher pour votre appel et sinon laisser le code d'origine, vous pouvez utiliser https://docs.python.org/3/library/unittest.mock.html#patch (puisque = 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!'
4
Risadinha

Dans le premier extrait, vous créez bar.do_something_expensive fait référence à l'objet fonction que a_package.baz.do_something_expensive se réfère à ce moment. Pour vraiment "monkeypatch" que vous auriez besoin de changer la fonction elle-même (vous changez seulement ce à quoi les noms se réfèrent); c'est possible, mais vous ne voulez pas vraiment le faire.

Dans vos tentatives pour modifier le comportement de a_function, vous avez fait deux choses:

  1. Dans la première tentative, vous faites de do_something_expensive un nom global dans votre module. Cependant, vous appelez a_function, qui ne regarde pas dans votre module pour résoudre les noms, donc il fait toujours référence à la même fonction.

  2. Dans le deuxième exemple, vous modifiez ce que a_package.baz.do_something_expensive fait référence à, mais bar.do_something_expensive n'y est pas magiquement lié. Ce nom fait toujours référence à l'objet fonction qu'il a recherché lors de son initialisation.

L'approche la plus simple mais loin d'être idéale serait de changer bar.py dire

import a_package.baz

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

La bonne solution est probablement l'une des deux choses:

  • Redéfinir a_function pour prendre une fonction en argument et l'appeler, plutôt que d'essayer de s'introduire et de changer à quelle fonction il est difficile de se référer, ou
  • Stockez la fonction à utiliser dans une instance d'une classe; c'est ainsi que nous faisons l'état mutable en Python.

L'utilisation de globaux (c'est ce qui change les choses au niveau du module à partir d'autres modules) est un mauvaise chose qui conduit à un code non maintenable, déroutant, non testable et non évolutif dont le flux est difficile à suivre.

3
Mike Graham

do_something_expensive Dans la fonction a_function() n'est qu'une variable dans l'espace de noms du module pointant vers un objet fonction. Lorsque vous redéfinissez le module, vous le faites dans un espace de noms différent.

0
DavidG