Я испытываю затруднения при замене функции от другого модуля с другой функцией, и это сводит меня с ума.
Скажем, у меня есть модуль 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!
Что я делаю неправильно?
Это может помочь подумать о том, как работают пространства имен 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_exurance в пространстве имен локального модуля или в пространстве имен a_package.baz, приведенном выше. Но поскольку bar импортирует do_something_exurance напрямую, а не ссылается на него из пространства имен модуля, вам нужно записать в его пространство имен:
import bar
bar.do_something_expensive = lambda: 'Something really cheap.'
В первом фрагменте вы указываете bar.do_something_exicing
для ссылки на объект функции, который a_package.baz.do_something_exicing
относится к этому моменту. Чтобы действительно "обезьянка", вам нужно было бы изменить саму функцию (вы меняете только то, к чему относятся имена); это возможно, но на самом деле вы не хотите этого делать.
В ваших попытках изменить поведение a_function
вы сделали две вещи:
В первой попытке вы сделали do_something_exicing глобальным именем в вашем модуле. Однако вы вызываете a_function
, которая не смотрит в ваш модуль для разрешения имен, поэтому по-прежнему относится к той же функции.
Во втором примере вы меняете то, к чему относится a_package.baz.do_something_exurities
, но bar.do_something_exurities
не привязан к нему магическим образом. Это имя по-прежнему относится к объекту функции, который он искал при инициализации.
Самый простой, но далеко не идеальный подход - изменить полосу .py
, чтобы сказать
import a_package.baz
def a_function():
print a_package.baz.do_something_expensive()
Правильным решением, вероятно, является одно из двух:
a_function
, чтобы принимать функцию в качестве аргумента и вызывать ее, а не пытаться проникнуть внутрь и изменить то, что функция, на которую она жестко запрограммирована, или Использование глобальных переменных (это то, что означает изменение уровня модуля по сравнению с другими модулями) - плохая вещь , которая приводит к не поддерживаемому, сбивающему с толку, непроверяемому, немасштабируемому коду, поток которого трудно отслеживать.
do_something_expensive
в функции a_function()
- это просто переменная в пространстве имен модуля, указывающая на объект функции. Когда вы переопределяете модуль, вы делаете это в другом пространстве имен.
Для этого есть очень элегантный декоратор: Гвидо ван Россум: Список Python-Dev: Monkeypatching Idioms.
Есть также пакет dectools, который я видел на PyCon 2010, который тоже может быть использован в этом контексте, но это может пойти в обратную сторону (обезьянье исправление на декларативном уровне методов... где вы не находитесь)
.