So entfernen Sie Dekoratoren von einer Funktion in Python

68

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 spamFunktion 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?

Herge
quelle

Antworten:

48

Im allgemeinen Fall können Sie nicht, weil

@with_connection
def spam(connection):
    # Do something

ist äquivalent zu

def spam(connection):
    # Do something

spam = with_connection(spam)

Dies bedeutet, dass der "ursprüngliche" Spam möglicherweise nicht mehr vorhanden ist. Ein (nicht zu hübscher) Hack wäre folgender:

def with_connection(f):
    def decorated(*args, **kwargs):
        f(get_connection(...), *args, **kwargs)
    decorated._original = f
    return decorated

@with_connection
def spam(connection):
    # Do something

spam._original(testcon) # calls the undecorated function
balpha
quelle
1
Wenn Sie den aufzurufenden Code ändern möchten, können _originalSie den Dekorateur auch kommentieren.
Eduffy
57

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 wrapsFunktion aus verwenden. functoolsSiehe 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
Alex Volkov
quelle
5
Python3 für den Gewinn!
Funk
1
Das ist falsch. Die dekorierte Funktion hat nur dann ein __wrapped__Attribut, wenn Sie sie mit dekorieren functools.wraps. Auch der Link ist tot.
Aran-Fey
Ich habe den Link zum Buch korrigiert und die Antwort für die Fälle bei der Implementierung eines eigenen Dekorateurs erweitert.
Alex Volkov
Das hat bei mir funktioniert. Zusätzliche Informationen: Wenn Ihre Funktion mehrere Dekoratoren hat, können Sie mehrere verketten .__wrapped__, um zur ursprünglichen Funktion zu gelangen.
Erik Kalkoken
33

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
jcdyer
quelle
1
Gibt es eine Möglichkeit, dies so zu erweitern, dass das Original mit der tiefsten Ebene an der äußersten dekorierten Funktion zugänglich ist, sodass ich für eine in drei Dekoratoren verpackte Funktion nicht ._original._original._original ausführen muss?
Sparr
@jcdyer Was genau bedeutet Dekorieren Ihrer Dekorateure ? Kann ich so etwas wie \ @include_original (nächste Zeile) \ @decorator_which_I_dont_control (nächste Zeile) function_definition ausführen?
Harshdeep
1
@ Harshdeep: Sie möchten so etwas tun now_i_control = include_original(decorator_i_dont_control)und dann Ihre Funktion mit dekorieren @now_i_control\ndef function():. Beachten Sie, dass dies y = foo(y)syntaktisch äquivalent zu ist @foo\ndef y():. Wenn Sie Ihren Vorschlag ausprobiert haben, haben Sie am Ende das include_original(decorator_i_dont_control(function)), was Sie wolleninclude_original(decorator_i_dont_control)(function)
jcdyer
1
@ Harshdeep Ich habe gerade meine Antwort mit Beispielverwendung bearbeitet. Wenn Sie den Dekorateur nicht selbst definiert haben, können Sie ihn mitdecorator = include_original(decorator)
jcdyer
@jcdyer Danke :) .. Ich kam auch zu einer ähnlichen Lösung def include_original(f): @wraps(f) def decorated(*args, **kwargs): return f(*args, **kwargs) decorated._original = f return decorated
Harshdeep
18

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.

Wojciech Bederski
quelle
4
func_closurewird durch __closure__in 3.x ersetzt und es ist bereits in 2.6
Ivan Baldin
1
Ich habe das gesehen, als ich mit Funktionen herumgespielt habe, aber es wird irgendwie kompliziert, wenn Sie mehr als einen Dekorator für eine Funktion verwenden. Sie rufen .func_closure[0].cell_contentsbis an cell_contents is None. Ich hatte auf eine elegantere Lösung gehofft.
Herge
Funktioniert wahrscheinlich nicht, wenn der Dekorateur functools.wraps verwendet
Evgen
Kam mit der gleichen Lösung, Kudos ^^ @EvgeniiPuchkaryov es scheint mit functools.wrap zu funktionieren
Romuald Brunet
10

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.

Oin
quelle
Genau das habe ich gesucht!
Samaspin
Epos! brauchte das auch.
ArielB
6

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 @decoratorSyntax tut - Sie können dann natürlich spam_fnormal auf das Original zugreifen .

dbr
quelle
Netter Ansatz, so klug!
Laike9m
Der springende Punkt bei der Verwendung eines Dekorateurs ist die einfache Wiederverwendung. Dies lässt sich nicht skalieren, wenn ich überall Code habe, der den Dekorateur benötigt.
dmvianna
5

Es ist eine gute Praxis, Dekorateure functools.wrapswie 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 verwenden inspect.unwrap:

>>> inspect.unwrap(spam)
<function spam at 0x7fe4e6dfc048>
Aran-Fey
quelle
3

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.

lyu.l
quelle
2

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.

Ameisen Aasma
quelle
1

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.

PaulMcG
quelle