Как одна обезьяна исправляет функцию в python? - PullRequest
73 голосов
/ 04 марта 2010

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

Допустим, у меня есть модуль 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 ]

83 голосов
/ 04 марта 2010

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

import bar
bar.do_something_expensive = lambda: 'Something really cheap.'
21 голосов
/ 04 марта 2010

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

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

4 голосов
/ 24 сентября 2014

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

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

Пытаясь изменить поведение a_function, вы сделали две вещи:

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

  2. Во втором примере вы меняете то, к чему относится 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.

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

0 голосов
/ 04 марта 2010

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

...