Як одна мавпа виправляє функцію в python?


80

У мене проблеми із заміною функції з іншого модуля на іншу, і це зводить мене з розуму.

Скажімо, у мене є модуль 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!

Що я роблю не так?


Другий не може працювати, оскільки ви просто перевизначили значення do_something_expensive у вашій локальній області. Однак я не знаю, чому 3-й не працює ...
pajton 03.03.10

1
Як пояснює Микола, ви копіюєте посилання і замінюєте лише одне з них. from module import non_module_memberі виправлення мавп на рівні модуля несумісні з цієї причини, і їх, як правило, найкраще уникати.
bobince 03.03.10

Переважною схемою іменування пакунків є мала літера без підкреслення, тобто apackage.
Mike Graham

1
@bobince, таких змінних станів на рівні модулів найкраще уникати, а погані наслідки глобалів вже давно визнані. Однак from foo import barце просто чудово і насправді рекомендується, коли це доречно.
Mike Graham

Відповіді:


87

Це може допомогти подумати про те, як працюють простори імен 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 вище. Але оскільки бар імпортує do_something_expensive безпосередньо, а не посилається на нього з простору імен модуля, вам потрібно написати в його простір імен:

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

А як щодо пакетів від сторонніх виробників, як тут ?
Тенгері

21

Для цього є справді елегантний декоратор: Гвідо ван Россум: Список Python-Dev: Ідіоми мавпани .

Існує також пакет dectools , який я бачив PyCon 2010, який, можливо, можна буде використовувати і в цьому контексті, але це може насправді піти іншим шляхом (мавпування на декларативному рівні методу ... там, де вас немає)


4
Ці декоратори, здається, не стосуються цієї справи.
Mike Graham

1
@MikeGraham: В електронній пошті Гвідо не згадується, що його прикладний код також дозволяє замінити будь-який метод, а не лише додати новий. Отже, я думаю, вони стосуються цієї справи.
tuomassalo

@MikeGraham Приклад Guido чудово працює для знущань над твердженням методу, я щойно спробував сам! setattr - це просто вигадливий спосіб сказати '='; Отже, "a = 3" або створює нову змінну під назвою "a", і встановлює її на три, або замінює значення існуючої змінної на 3
Chris Huang-Leaver

6

Якщо ви хочете лише виправити його для свого дзвінка, а в іншому випадку залишити вихідний код, ви можете використовувати 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

У першому фрагменті ви робите bar.do_something_expensiveпосилання на об'єкт функції, який a_package.baz.do_something_expensiveпосилається на той момент. Насправді "monkeypatch", що вам потрібно було б змінити саму функцію (ви змінюєте лише те, що стосується імен); це можливо, але ви насправді не хочете цього робити.

У своїх спробах змінити поведінку a_function, ви зробили дві речі:

  1. З першої спроби ви робите do_something_expensive глобальним ім'ям у своєму модулі. Однак ви телефонуєте 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

do_something_expensiveу a_function()функції - це просто змінна в просторі імен модуля, що вказує на об'єкт функції. Коли ви перевизначаєте модуль, ви робите це в іншому просторі імен.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.