Wie patcht ein Affe eine Funktion in Python?

80

Ich habe Probleme, eine Funktion aus einem anderen Modul durch eine andere Funktion zu ersetzen, und das macht mich verrückt.

Angenommen, ich habe ein Modul bar.py, das so aussieht:

from a_package.baz import do_something_expensive

def a_function():
    print do_something_expensive()

Und ich habe ein anderes Modul, das so aussieht:

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

Ich würde erwarten, die Ergebnisse zu erhalten:

Something expensive!
Something really cheap.
Something really cheap.

Aber stattdessen bekomme ich folgendes:

Something expensive!
Something expensive!
Something expensive!

Was mache ich falsch?

Guidoismus
quelle
Der zweite kann nicht funktionieren, weil Sie nur die Bedeutung von do_something_expensive in Ihrem lokalen Bereich neu definieren. Ich weiß jedoch nicht, warum der 3. nicht funktioniert ...
pajton
1
Wie Nicholas erklärt, kopieren Sie eine Referenz und ersetzen nur eine der Referenzen. from module import non_module_memberund Affen-Patches auf Modulebene sind aus diesem Grund nicht kompatibel und werden im Allgemeinen am besten vermieden.
Bobince
Das bevorzugte Paketbenennungsschema ist Kleinbuchstaben ohne Unterstriche, d apackage. H.
Mike Graham
1
@bobince, ein solcher veränderlicher Zustand auf Modulebene wird am besten vermieden, wobei die schlimmen Folgen von Globals längst erkannt werden. Dies from foo import barist jedoch in Ordnung und wird gegebenenfalls empfohlen.
Mike Graham

Antworten:

87

Es kann hilfreich sein, darüber nachzudenken, wie Python-Namespaces funktionieren: Sie sind im Wesentlichen Wörterbücher. Wenn Sie dies tun:

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

Betrachten Sie es so:

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

Hoffentlich können Sie erkennen , warum dies nicht funktioniert , dann :-) Wenn Sie einen Namen in einem Namensraum zu importieren, wird der Wert des Namens im Namensraum Sie importiert aus irrelevant ist. Sie ändern den Wert von do_something_expensive nur im Namespace des lokalen Moduls oder im Namespace von a_package.baz oben. Da Bar do_something_expensive direkt importiert, anstatt es aus dem Modul-Namespace zu referenzieren, müssen Sie in seinen Namespace schreiben:

import bar
bar.do_something_expensive = lambda: 'Something really cheap.'
Nicholas Riley
quelle
Was ist mit Paketen von Drittanbietern, wie hier ?
Tengerye
21

Dafür gibt es einen wirklich eleganten Dekorateur: Guido van Rossum: Python-Dev-Liste: Monkeypatching Idioms .

Es gibt auch das Dectools- Paket, bei dem ich eine PyCon 2010 gesehen habe, die möglicherweise auch in diesem Kontext verwendet werden kann, aber das könnte auch in die andere Richtung gehen (Monkeypatching auf der Ebene der Methodendeklaration ... wo Sie nicht sind).

RyanWilcox
quelle
4
Diese Dekorateure scheinen auf diesen Fall nicht zuzutreffen.
Mike Graham
1
@ MikeGraham: Guidos E-Mail erwähnt nicht, dass sein Beispielcode auch das Ersetzen einer Methode ermöglicht, nicht nur das Hinzufügen einer neuen. Ich denke, sie gelten für diesen Fall.
Tuomassalo
@ MikeGraham Das Guido-Beispiel funktioniert perfekt, um eine Methodenanweisung zu verspotten. Ich habe es einfach selbst versucht! setattr ist nur eine ausgefallene Art, '=' zu sagen; 'A = 3' erstellt also entweder eine neue Variable namens 'a' und setzt sie auf drei oder ersetzt den Wert einer vorhandenen Variablen durch 3
Chris Huang-Leaver
6

Wenn Sie es nur für Ihren Anruf patchen und ansonsten den Originalcode beibehalten möchten, können Sie https://docs.python.org/3/library/unittest.mock.html#patch (seit Python 3.3) verwenden:

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
quelle
3

Im ersten Snippet bar.do_something_expensiveverweisen Sie auf das Funktionsobjekt, a_package.baz.do_something_expensiveauf das in diesem Moment verwiesen wird. Um wirklich "monkeypatch" zu machen, müssten Sie die Funktion selbst ändern (Sie ändern nur, worauf sich die Namen beziehen); Das ist möglich, aber das wollen Sie eigentlich nicht.

Bei Ihren Versuchen, das Verhalten von zu ändern a_function, haben Sie zwei Dinge getan:

  1. Im ersten Versuch machen Sie do_something_expensive zu einem globalen Namen in Ihrem Modul. Sie rufen jedoch auf a_function, das in Ihrem Modul nicht nach Auflösungsnamen sucht, sodass es immer noch auf dieselbe Funktion verweist.

  2. Im zweiten Beispiel ändern Sie das, worauf es sich a_package.baz.do_something_expensivebezieht, sind aber bar.do_something_expensivenicht magisch daran gebunden. Dieser Name bezieht sich immer noch auf das Funktionsobjekt, nach dem es beim Initialisieren gesucht hat.

Der einfachste, aber alles andere als ideale Ansatz wäre, sich zu ändern, um bar.pyzu sagen

import a_package.baz

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

Die richtige Lösung ist wahrscheinlich eines von zwei Dingen:

  • Definieren Sie neu a_function, um eine Funktion als Argument zu verwenden und dies zu nennen, anstatt zu versuchen, sich einzuschleichen und zu ändern, auf welche Funktion es schwer zu codieren ist, oder
  • Speichern Sie die Funktion, die in einer Instanz einer Klasse verwendet werden soll. So machen wir den veränderlichen Zustand in Python.

Die Verwendung von Globals (das ist es, was das Ändern von Dingen auf Modulebene von anderen Modulen ist) ist eine schlechte Sache , die zu nicht wartbarem, verwirrendem, nicht testbarem, nicht skalierbarem Code führt, dessen Fluss schwer zu verfolgen ist.

Mike Graham
quelle
0

do_something_expensivein der a_function()Funktion ist nur eine Variable im Namespace des Moduls, die auf ein Funktionsobjekt verweist. Wenn Sie das Modul neu definieren, tun Sie dies in einem anderen Namespace.

DavidG
quelle