Как одна обезьяна исправляет функцию в 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!

что я делаю не так?

5 66

5 ответов:

это может помочь подумать о том, как работают пространства имен 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.бэз по пространства имен выше. Но поскольку bar импортирует do_something_expensive напрямую, а не ссылается на него из пространства имен модуля, вам нужно написать в его пространство имен:

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

есть действительно элегантный декоратор для этого:Guido van Rossum: Python-Dev list: Monkeypatching Idioms.

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

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

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

  1. С первой попытки вы делаете do_something_expensive глобальным имя в вашем модуле. Тем не менее, вы звоните a_function, который не выглядит в вашем модуле для разрешения имен, поэтому он по-прежнему относится к той же функции.

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

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

import a_package.baz

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

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

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

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

Если вы хотите только исправить его для вашего вызова и в противном случае оставить исходный код вы можете использовать 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!'

do_something_expensive на a_function() функции только переменной в пространстве имен модуля указывает на объект function. При переопределении модуля вы делаете это в другом пространстве имен.