Как одна обезьяна исправляет функцию в 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()
- это просто переменная в пространстве имен модуля, указывающая на объект функции. Когда вы переопределяете модуль, вы делаете это в другом пространстве имен.