2009-07-22 10 views
44

Powiedzmy mam następujące:Jak strip dekoratorów z funkcją w python

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 przechodzenia przez kłopotów konfigurowania połączenia (lub cokolwiek dekorator robi).

Biorąc pod uwagę spam, jak mogę usunąć dekorator z niego i uzyskać podstawową funkcję "nieodkrytą"?

Odpowiedz

30

W ogólnym przypadku nie można, bo

@with_connection 
def spam(connection): 
    # Do something 

jest równoważna

def spam(connection): 
    # Do something 

spam = with_connection(spam) 

co oznacza, że ​​"oryginał" spam może nawet nie istnieje. A (nie za bardzo) włamywanie byłoby to:

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

Jeśli zamierzasz zmodyfikować kod, aby wywołać '_original', możesz równie dobrze skomentować dekoratora. – eduffy

+2

@eduffy: Właśnie o to pytam. – balpha

+0

Masz rację ... Nie myślałem o tym w sensie testowym. – eduffy

14

Oto FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:

orig_spam = spam.func_closure[0].cell_contents 

Edit: Dla funkcji/metod urządzone więcej niż jeden raz i bardziej skomplikowanych dekoratorów można spróbować użyć następujący kod. Polega na tym, że urządzone funkcje są __name__d inne niż pierwotna funkcja.

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> 

To jednak nie jest głupi dowód. Nie powiedzie się, jeśli nazwa funkcji zwróconej przez dekoratora jest taka sama, jak nazwa dekorowanego. Kolejność sprawdzeń hasattr() jest również heurystyczna, istnieją łańcuchy dekoracji, które zwracają błędne wyniki. Rozwiązanie

+3

'func_closure' jest zastępowane przez' __closure__' w wersji 3.x i już jest w wersji 2.6 –

+1

Widziałem to, kiedy bawiłem się funkcjami, ale to staje się skomplikowane, jeśli używasz więcej niż jednego dekoratora w funkcji. Wywołujesz '.func_closure [0] .cell_contents', dopóki' cell_contents nie będzie miało wartości '. Miałem nadzieję na bardziej eleganckie rozwiązanie. – Herge

+0

Prawdopodobnie nie zadziała, jeśli dekorator użyje functools.wraps –

27

balpha może być bardziej uogólnić z tym meta-dekoratora:

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

Następnie można ozdobić swoje dekoratorów z @include_original, a każdy będzie mieć sprawdzalne (bez dekoracji) wersja schowany wewnątrz niego.

@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 
+4

Teraz rozmawiamy. metadekoracja FTW. – brice

+1

Czy istnieje sposób na przedłużenie tego tak, aby najgłębszy poziom oryginału był dostępny w najbardziej zewnętrznej funkcji dekorowanej, więc nie muszę tego robić. Oryginalna. Oryginalna dla funkcji zawijanej w trzech dekoratorach? – Sparr

+0

@jcdyer Co dokładnie __dekorować dekoratorów__ oznacza? Czy mogę zrobić coś w stylu \ @include_original (następna linia) \ @decorator_which_I_dont_control (następna linia) function_definition? – Harshdeep

2

Zwykłe podejście do testowania takich funkcji jest, aby wszelkie zależności, takie jak get_connection, konfigurowalne. Następnie można go zastąpić próbą podczas testowania. Zasadniczo to samo, co wstrzyknięcie zależności w świecie Java, ale znacznie prostsze dzięki dynamicznej naturze Pythona.

Kod dla niego może wyglądać 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" 

zależności od kodu można znaleźć lepsze niż obiekt dekoratora trzymać funkcji fabrycznych. Problem związany z tym, że jest na dekoratorze, należy pamiętać, aby przywrócić go do starej wartości w metodzie przerwania.

6

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żna po prostu zrobić ..

def with_connection(f): 
    .... 

def spam_f(connection): 
    ... 

spam = with_connection(spam_f) 

..czyli wszystko Składnia @decorator robi - można to oczywiście dostęp do oryginalnego spam_f normalnie

+0

Przyjemne podejście, takie sprytne! – laike9m

1

Dodaj dekorator nie robić nic:

def do_nothing(f): 
    return f 

Po zdefiniowaniu lub importowania with_connection ale zanim się do metod, które go używają jako dekorator, dodać:

if TESTING: 
    with_connection = do_nothing 

Następnie jeśli ustawić globalną Pilotowego To prawda, trzeba będzie zastąpiona with_connection z nicnierobienia dekoratora.

16

Trochę aktualizacji tego pytania. Jeśli używasz Pythona 3, możesz użyć właściwości __wrapped__, która zwraca funkcję zawijaną.

Oto przykład z Python Cookbook, 3rd edition

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

Zobacz dyskusję na bardziej szczegółowym używania tego atrybutu.

+0

Python3 za wygraną! – funk

4

Teraz można użyć pakietu undecorated:

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

to idzie przez kłopotów kopanie przez wszystkich warstw różnych dekoratorów aż osiągnie dolny funkcji i nie wymaga wymiany oryginalne dekoratorów. Działa zarówno na python2, jak i python3.