Angenommen, ich habe Folgendes:
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
return decorated
@with_connection
def spam(connection):
# Do something
Ich möchte die spam
Funktion testen , ohne die Mühe zu haben, eine Verbindung herzustellen (oder was auch immer der Dekorateur tut).
In Anbetracht spam
, wie Streifen ich den Dekorateur aus ihm und den zugrunde liegenden „undecorated“ -Funktion erhalten?
_original
Sie den Dekorateur auch kommentieren.Für diese Frage wurde ein kleines Update durchgeführt. Wenn Sie Python 3 verwenden, können Sie die
__wrapped__
Eigenschaft für Dekoratoren aus stdlib verwenden.Hier ist ein Beispiel aus Python Cookbook, 3. Ausgabe, Abschnitt 9.3 Auspacken von Dekorateuren
>>> @somedecorator >>> def add(x, y): ... return x + y ... >>> orig_add = add.__wrapped__ >>> orig_add(3, 4) 7 >>>
Wenn Sie versuchen, eine Funktion aus dem benutzerdefinierten Dekorator zu entpacken, muss die Dekoratorfunktion die
wraps
Funktion aus verwenden.functools
Siehe Diskussion in Python Cookbook, 3. Ausgabe, Abschnitt 9.2. Funktionsmetadaten beim Schreiben von Dekoratoren beibehalten>>> from functools import wraps >>> def somedecoarator(func): ... @wraps(func) ... def wrapper(*args, **kwargs): ... # decorator implementation here ... # ... ... return func(*args, kwargs) ... ... return wrapper
quelle
__wrapped__
Attribut, wenn Sie sie mit dekorierenfunctools.wraps
. Auch der Link ist tot..__wrapped__
, um zur ursprünglichen Funktion zu gelangen.Die Lösung von balpha kann mit diesem Meta-Dekorator verallgemeinerbar gemacht werden:
def include_original(dec): def meta_decorator(f): decorated = dec(f) decorated._original = f return decorated return meta_decorator
Dann können Sie Ihre Dekorateure mit @include_original dekorieren, und jeder hat eine testbare (nicht dekorierte) Version darin versteckt.
@include_original def shout(f): def _(): string = f() return string.upper() return _ @shout def function(): return "hello world" >>> print function() HELLO_WORLD >>> print function._original() hello world
quelle
now_i_control = include_original(decorator_i_dont_control)
und dann Ihre Funktion mit dekorieren@now_i_control\ndef function():
. Beachten Sie, dass diesy = foo(y)
syntaktisch äquivalent zu ist@foo\ndef y():
. Wenn Sie Ihren Vorschlag ausprobiert haben, haben Sie am Ende dasinclude_original(decorator_i_dont_control(function))
, was Sie wolleninclude_original(decorator_i_dont_control)(function)
decorator = include_original(decorator)
def include_original(f): @wraps(f) def decorated(*args, **kwargs): return f(*args, **kwargs) decorated._original = f return decorated
Siehe, FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:
orig_spam = spam.func_closure[0].cell_contents
Bearbeiten : Für Funktionen / Methoden, die mehrmals dekoriert wurden und mit komplizierteren Dekoratoren können Sie versuchen, den folgenden Code zu verwenden. Es beruht auf der Tatsache, dass dekorierte Funktionen __name__d anders sind als die ursprüngliche Funktion.
def search_for_orig(decorated, orig_name): for obj in (c.cell_contents for c in decorated.__closure__): if hasattr(obj, "__name__") and obj.__name__ == orig_name: return obj if hasattr(obj, "__closure__") and obj.__closure__: found = search_for_orig(obj, orig_name) if found: return found return None >>> search_for_orig(spam, "spam") <function spam at 0x027ACD70>
Es ist jedoch kein Kinderspiel. Es schlägt fehl, wenn der Name der von einem Dekorateur zurückgegebenen Funktion mit dem Namen der dekorierten Funktion übereinstimmt. Die Reihenfolge der hasattr () - Prüfungen ist ebenfalls eine Heuristik. Es gibt Dekorationsketten, die auf jeden Fall falsche Ergebnisse liefern.
quelle
func_closure
wird durch__closure__
in 3.x ersetzt und es ist bereits in 2.6.func_closure[0].cell_contents
bis ancell_contents is None
. Ich hatte auf eine elegantere Lösung gehofft.Sie können jetzt das nicht dekorierte Paket verwenden:
>>> from undecorated import undecorated >>> undecorated(spam)
Es geht durch den Aufwand, alle Schichten verschiedener Dekorateure zu durchsuchen, bis die untere Funktion erreicht ist und die ursprünglichen Dekorateure nicht mehr gewechselt werden müssen. Es funktioniert sowohl mit Python 2 als auch mit Python 3.
quelle
Anstatt zu tun ...
def with_connection(f): def decorated(*args, **kwargs): f(get_connection(...), *args, **kwargs) return decorated @with_connection def spam(connection): # Do something orig_spam = magic_hack_of_a_function(spam)
Sie könnten einfach tun ...
def with_connection(f): ... def spam_f(connection): ... spam = with_connection(spam_f)
... das ist alles, was die
@decorator
Syntax tut - Sie können dann natürlichspam_f
normal auf das Original zugreifen .quelle
Es ist eine gute Praxis, Dekorateure
functools.wraps
wie folgt zu dekorieren :import functools def with_connection(f): @functools.wraps(f) def decorated(*args, **kwargs): f(get_connection(...), *args, **kwargs) return decorated @with_connection def spam(connection): # Do something
Ab Python 3.2 wird automatisch ein
__wrapped__
Attribut hinzugefügt , mit dem Sie die ursprüngliche, nicht dekorierte Funktion abrufen können:>>> spam.__wrapped__ <function spam at 0x7fe4e6dfc048>
Anstatt jedoch manuell auf das
__wrapped__
Attribut zuzugreifen , ist es besser, Folgendes zu verwendeninspect.unwrap
:>>> inspect.unwrap(spam) <function spam at 0x7fe4e6dfc048>
quelle
Die ursprüngliche Funktion wird in gespeichert
spam.__closure__[0].cell_contents
.Decorator verwendet den Verschluss, um die ursprüngliche Funktion mit einer zusätzlichen Funktionsebene zu verbinden. Die ursprüngliche Funktion muss in einer Verschlusszelle gespeichert werden, die von einer der Funktionen in der verschachtelten Struktur des Dekorateurs verwaltet wird.
Beispiel:
>>> def add(f): ... def _decorator(*args, **kargs): ... print('hello_world') ... return f(*args, **kargs) ... return _decorator ... >>> @add ... def f(msg): ... print('f ==>', msg) ... >>> f('alice') hello_world f ==> alice >>> f.__closure__[0].cell_contents <function f at 0x7f5d205991e0> >>> f.__closure__[0].cell_contents('alice') f ==> alice
Dies ist das Kernprinzip von nicht dekoriert. Weitere Informationen finden Sie im Quellcode.
quelle
Der übliche Ansatz zum Testen solcher Funktionen besteht darin, Abhängigkeiten wie get_connection konfigurierbar zu machen. Dann können Sie es beim Testen mit einem Mock überschreiben. Im Grunde das gleiche wie die Abhängigkeitsinjektion in der Java-Welt, aber dank Pythons dynamischer Natur viel einfacher.
Der Code dafür könnte ungefähr so aussehen:
# decorator definition def with_connection(f): def decorated(*args, **kwargs): f(with_connection.connection_getter(), *args, **kwargs) return decorated # normal configuration with_connection.connection_getter = lambda: get_connection(...) # inside testsuite setup override it with_connection.connection_getter = lambda: "a mock connection"
Abhängig von Ihrem Code könnten Sie ein besseres Objekt als den Dekorateur finden, um die Werksfunktion zu aktivieren. Das Problem beim Dekorieren ist, dass Sie daran denken müssen, den alten Wert in der Teardown-Methode wiederherzustellen.
quelle
Fügen Sie einen Nicht-Dekorator hinzu:
def do_nothing(f): return f
Fügen Sie nach dem Definieren oder Importieren von with_connection, bevor Sie zu den Methoden gelangen, die es als Dekorator verwenden, Folgendes hinzu:
if TESTING: with_connection = do_nothing
Wenn Sie dann den globalen TEST auf True setzen, haben Sie with_connection durch einen Do-nothing-Dekorator ersetzt.
quelle