W jaki sposób jedna małpa łata funkcję w Pythonie?

80

Mam problem z zastąpieniem funkcji z innego modułu inną funkcją i doprowadza mnie to do szału.

Powiedzmy, że mam moduł bar.py, który wygląda następująco:

from a_package.baz import do_something_expensive

def a_function():
    print do_something_expensive()

Mam inny moduł, który wygląda tak:

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()

Spodziewałbym się wyników:

Something expensive!
Something really cheap.
Something really cheap.

Ale zamiast tego otrzymuję to:

Something expensive!
Something expensive!
Something expensive!

Co ja robię źle?

guidoizm
źródło
Drugi nie może działać, ponieważ przedefiniowałeś znaczenie do_something_expensive w swoim lokalnym zakresie. Nie wiem jednak, dlaczego trzecia nie działa ...
pajton
1
Jak wyjaśnia Nicholas, kopiujesz odniesienie i zastępujesz tylko jeden z nich. from module import non_module_memberz tego powodu małpa i łatanie na poziomie modułu są niekompatybilne i ogólnie najlepiej ich unikać.
bobince
Preferowany schemat nazewnictwa pakietów składa się z małych liter bez znaków podkreślenia, np apackage.
Mike Graham
1
@bobince, najlepiej unikać stanu zmiennego na poziomie modułu, takiego jak ten, ze złymi konsekwencjami globalnych dawno rozpoznanych. Jednak from foo import barjest dobrze i faktycznie zalecane, gdy stosowne.
Mike Graham

Odpowiedzi:

87

Warto pomyśleć o tym, jak działają przestrzenie nazw Pythona: są to w zasadzie słowniki. Więc kiedy to zrobisz:

from a_package.baz import do_something_expensive
do_something_expensive = lambda: 'Something really cheap.'

Pomyśl o tym w ten sposób:

do_something_expensive = a_package.baz['do_something_expensive']
do_something_expensive = lambda: 'Something really cheap.'

Mam nadzieję, że zdajesz sobie sprawę, dlaczego to nie działa wtedy :-) Po zaimportowaniu nazwę w obszarze nazw, wartość nazwy w obszarze nazw ty importowanego z ma znaczenia. Zmieniasz tylko wartość do_something_expensive w przestrzeni nazw modułu lokalnego lub w przestrzeni nazw a_package.baz powyżej. Ale ponieważ bar importuje do_something_expensive bezpośrednio, zamiast odwoływać się do niego z przestrzeni nazw modułu, musisz napisać do jego przestrzeni nazw:

import bar
bar.do_something_expensive = lambda: 'Something really cheap.'
Nicholas Riley
źródło
A co z paczkami od osób trzecich, jak tutaj ?
Tengerye
21

Jest do tego naprawdę elegancki dekorator: Guido van Rossum: Python-Dev list: Monkeypatching Idioms .

Istnieje również pakiet dectools , który widziałem w PyCon 2010, który może być również używany w tym kontekście, ale może to faktycznie pójść w drugą stronę (małpa dopasowywanie na poziomie deklaratywnym metody ... gdzie nie jesteś)

RyanWilcox
źródło
4
Wydaje się, że te dekoratory nie mają zastosowania w tym przypadku.
Mike Graham
1
@MikeGraham: E-mail Guido nie wspomina, że ​​jego przykładowy kod pozwala również na zastąpienie dowolnej metody, a nie tylko dodanie nowej. Więc myślę, że mają one zastosowanie w tym przypadku.
tuomassalo
@MikeGraham Przykład Guido doskonale sprawdza się w kpieniu z instrukcji metody, po prostu wypróbowałem to sam! setattr to po prostu fantazyjny sposób powiedzenia „=”; Zatem „a = 3” albo tworzy nową zmienną o nazwie „a” i ustawia ją na trzy, albo zastępuje wartość istniejącej zmiennej wartością 3
Chris Huang-Leaver
6

Jeśli chcesz załatać go tylko dla swojego wywołania, a poza tym pozostawić oryginalny kod, możesz użyć https://docs.python.org/3/library/unittest.mock.html#patch (od Pythona 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!'
Risadinha
źródło
3

W pierwszym fragmencie bar.do_something_expensiveodwołujesz się do obiektu funkcji, a_package.baz.do_something_expensivedo którego odwołuje się w tym momencie. Aby naprawdę "monkeypatch", musiałbyś zmienić samą funkcję (zmieniasz tylko to, do czego odnoszą się nazwy); jest to możliwe, ale tak naprawdę nie chcesz tego robić.

Próbując zmienić zachowanie programu a_function, zrobiłeś dwie rzeczy:

  1. W pierwszej próbie ustawiasz w swoim module globalną nazwę do_something_expensive. Jednak dzwonisz a_function, co nie szuka w Twoim module nazw, więc nadal odwołuje się do tej samej funkcji.

  2. W drugim przykładzie zmieniasz to, a_package.baz.do_something_expensivedo czego się odnosi, ale bar.do_something_expensivenie jesteś z tym magicznie związany. Ta nazwa nadal odnosi się do obiektu funkcji, który wyszukał podczas inicjalizacji.

Najprostszym, ale daleko od idealnych rozwiązaniem byłoby zmienić bar.py, aby powiedzieć

import a_package.baz

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

Prawidłowe rozwiązanie to prawdopodobnie jedna z dwóch rzeczy:

  • Przedefiniuj, a_functionaby przyjąć funkcję jako argument i wywołać ją, zamiast próbować wkraść się i zmienić funkcję, do której trudno jest się odnieść, lub
  • Przechowuj funkcję, która ma być używana w instancji klasy; w ten sposób robimy zmienny stan w Pythonie.

Używanie globali (tym jest zmiana elementów na poziomie modułu z innych modułów) jest złą rzeczą, która prowadzi do niemożliwego do utrzymania, zagmatwania, nietestowalnego, nieskalowalnego kodu, którego przepływ jest trudny do śledzenia.

Mike Graham
źródło
0

do_something_expensivew a_function()funkcji jest po prostu zmienną w przestrzeni nazw modułu wskazującą na obiekt funkcji. Kiedy przedefiniowujesz moduł, robisz to w innej przestrzeni nazw.

DavidG
źródło