Jak usunąć dekoratory z funkcji w Pythonie

Powiedzmy, że mam:

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

@with_connection
def spam(connection):
    # Do something

Chcę przetestować funkcję spam bez konieczności konfigurowania połączenia (lub cokolwiek robi dekorator).

Biorąc pod uwagę spam, Jak usunąć dekorator z niego i uzyskać podstawową funkcję "niedekorowaną"?

Author: Peter Mortensen, 2009-07-22

10 answers

W ogólnym przypadku, nie możesz, ponieważ

@with_connection
def spam(connection):
    # Do something

Jest równoważne

def spam(connection):
    # Do something

spam = with_connection(spam)

Co oznacza, że "oryginalny" spam może już nie istnieć. A (nie za ładny) hack byłby taki:

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
 34
Author: balpha,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-07-22 15:42:28

Rozwiązanie Balpha może być bardziej uogólnione za pomocą tego meta-dekoratora:

def include_original(dec):
    def meta_decorator(f):
        decorated = dec(f)
        decorated._original = f
        return decorated
    return meta_decorator

Następnie możesz udekorować swoich dekoratorów za pomocą @include_original, a każdy z nich będzie miał testowalną (niedekorowaną) wersję schowaną w środku.

@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
 28
Author: jcdyer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-21 10:15:43

Pojawił się mały update dla tego pytania. Jeśli używasz Pythona 3, możesz użyć właściwości __wrapped__ dla dekoratorów ze stdlib.

Oto przykład z Python Cookbook, wydanie 3, sekcja 9.3 rozpakowywanie dekoratorów

>>> @somedecorator
>>> def add(x, y):
...     return x + y
...
>>> orig_add = add.__wrapped__
>>> orig_add(3, 4)
7
>>>

Jeśli próbujesz rozpakować funkcję z niestandardowego dekoratora, funkcja dekoratora musi użyć wraps funkcji z functools Zobacz dyskusję w Python Cookbook, wydanie 3, sekcja 9.2 zachowując metadane funkcji podczas pisania dekoratorzy

>>> from functools import wraps
>>> def somedecoarator(func):
...    @wraps(func)
...    def wrapper(*args, **kwargs):
...       # decorator implementation here
...       # ...
...       return func(*args, kwargs)
...
...    return wrapper
 26
Author: Alex Volkov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-09 21:30:21

Oto Fuglyhackthatwill Work Foryourexamplebuticantpromiseanythingelse:

 orig_spam = spam.func_closure[0].cell_contents

Edit: W przypadku funkcji / metod dekorowanych więcej niż raz i bardziej skomplikowanych dekoratorów możesz spróbować użyć poniższego kodu. Polega ona na tym, że funkcje dekorowane są __name__d inaczej niż funkcja oryginalna.

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>
Nie jest to jednak głupi dowód. Nie powiedzie się, jeśli nazwa funkcji zwróconej przez dekoratora jest taka sama jak dekorowana. Order of sprawdzanie hasattr () jest również heurystyką, istnieją łańcuchy dekoracji, które zwracają błędne wyniki w każdym przypadku.
 17
Author: Wojciech Bederski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-07-22 16:59:05

Zamiast robić...

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)
Możesz to zrobić...
def with_connection(f):
    ...

def spam_f(connection):
    ...

spam = with_connection(spam_f)

...czyli wszystko, co robi składnia @decorator - wtedy możesz oczywiście uzyskać dostęp do oryginału spam_f normalnie.

 6
Author: dbr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-20 14:36:57

Możesz teraz użyć pakietu undekorated :

>>> from undecorated import undecorated
>>> undecorated(spam)

Przechodzi przez kłopotliwe przekopywanie się przez wszystkie warstwy różnych dekoratorów, dopóki nie osiągnie dolnej funkcji i nie wymaga zmiany oryginalnych dekoratorów. Działa zarówno na Pythonie 2, jak i Pythonie 3.

 6
Author: Oin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-20 14:40:03

Zwykłe podejście do testowania takich funkcji polega na tym, aby dowolne zależności, takie jak get_connection, były konfigurowalne. Następnie możesz obejść go za pomocą makiety podczas testowania. Zasadniczo to samo co dependency injection w świecie Javy, ale o wiele prostsze dzięki dynamicznej naturze Pythons.

Kod może wyglądać mniej więcej tak:

# 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"

W zależności od kodu można znaleźć lepszy obiekt niż dekorator do przyklejenia funkcji fabrycznej. Problem z posiadaniem go na dekorator jest to, że trzeba pamiętać, aby przywrócić go do starej wartości w metodzie teardown.

 2
Author: Ants Aasma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-07-22 21:43:04

Dodaj dekoratora do niczego:

def do_nothing(f):
    return f

Po zdefiniowaniu lub zaimportowaniu with_connection, ale zanim przejdziesz do metod, które używają go jako dekoratora, dodaj:

if TESTING:
    with_connection = do_nothing

Wtedy, jeśli ustawisz global TESTING na True, zastąpisz with_connection dekoratorem do-nothing.

 1
Author: PaulMcG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-09-19 08:23:38

Dobrą praktyką jest dekorowanie dekoratorów z functools.wraps tak:

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

Począwszy od Pythona 3.2, to automatycznie doda atrybut __wrapped__, który pozwala pobrać oryginalną, niedekorowaną funkcję:

>>> spam.__wrapped__
<function spam at 0x7fe4e6dfc048>

Jednak zamiast ręcznego dostępu do atrybutu __wrapped__, lepiej użyć inspect.unwrap:

>>> inspect.unwrap(spam)
<function spam at 0x7fe4e6dfc048>
 1
Author: Aran-Fey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-20 14:40:50

Oryginalna funkcja jest przechowywana w spam.__closure__[0].cell_contents.
Dekorator wykorzystuje zamknięcie, aby powiązać oryginalną funkcję z dodatkową warstwą funkcjonalności. Pierwotna funkcja musi być przechowywana w komórce zamykającej utrzymywanej przez jedną z funkcji w zagnieżdżonej strukturze dekoratora.
Przykład:

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

Jest to podstawowa zasada undekorated , możesz odwołać się do kodu źródłowego po więcej szczegółów.

 1
Author: lyu.l,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-24 11:39:12