Как одна обезьяна исправляет функцию в python?

У меня возникли проблемы с заменой функции из другого модуля другой функцией, и это сводит меня с ума.

Скажем, у меня есть модуль bar.py, который выглядит так:

from a_package.baz import do_something_expensive

def a_function():
    print do_something_expensive()

И у меня есть еще один модуль, который выглядит так:

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

Я ожидаю получить результаты:

Something expensive!
Something really cheap.
Something really cheap.

Но вместо этого я получаю следующее:

Something expensive!
Something expensive!
Something expensive!

Что я делаю неправильно?

Ответ 1

Это может помочь подумать о том, как работают пространства имен Python: они по сути являются словарями. Поэтому, когда вы это сделаете:

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

подумайте об этом так:

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

Надеюсь, вы поймете, почему это не работает тогда:-) Как только вы импортируете имя в пространство имен, значение имени в импортируемом им пространстве имен не имеет значения. Вы изменяете значение do_something_expensive в пространстве имен локального модуля или в пространстве имен a_package.baz, выше. Но поскольку bar импортирует do_something_expensive напрямую, а не ссылается на него из пространства имен модулей, вам нужно записать его в пространство имен:

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

Ответ 2

Там действительно элегантный декоратор для этого: Гвидо ван Россум: список Python-Dev: обезьянники идиомы.

Также существует пакет dectools, который я видел в PyCon 2010, который также может быть использован в этом контексте, но что на самом деле может пойти другим путем (monkeypatching на декларативном уровне метода..., где вы не находитесь)

Ответ 3

В первом фрагменте вы делаете bar.do_something_expensive ссылкой на объект функции, к которому в данный момент относится a_package.baz.do_something_expensive. На самом деле "monkeypatch", что вам нужно будет изменить сама функция (вы меняете только имена, на которые ссылаются); это возможно, но на самом деле вы этого не хотите.

В ваших попытках изменить поведение a_function, вы сделали две вещи:

  • В первую очередь вы делаете do_something_expensive глобальное имя в вашем модуле. Однако вы вызываете a_function, который не ищет в вашем модуле разрешения имен, поэтому он все равно относится к одной и той же функции.

  • Во втором примере вы меняете то, что относится к a_package.baz.do_something_expensive, но bar.do_something_expensive не привязано к нему магией. Это имя по-прежнему относится к объекту функции, который он искал, когда он был инициализирован.

Самый простой, но далеко не идеальный подход - изменить bar.py на

import a_package.baz

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

Правильное решение, вероятно, является одной из двух вещей:

  • Переопределить a_function, чтобы принять функцию в качестве аргумента и вызвать это, вместо того, чтобы пытаться проникнуть внутрь и изменить, какую функцию он жестко закодирован, или
  • Сохранить функцию, которая будет использоваться в экземпляре класса; это то, как мы выполняем изменчивое состояние в Python.

Использование глобальных переменных (это то, что изменяет материал на уровне модулей из других модулей) - это плохая вещь, которая ведет к недостижимому, запутанному, неутешимому, незаметному коду, поток которого трудно отслеживать.

Ответ 4

Если вы хотите только исправить его для своего вызова и иначе оставить исходный код, вы можете использовать https://docs.python.org/3/library/unittest.mock.html#patch (начиная с 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!'

Ответ 5

do_something_expensive в функции a_function() - это просто переменная в пространстве имен модуля, указывающая на объект функции. Когда вы переопределяете модуль, вы делаете это в другом пространстве имен.