2009-07-22 8 views
44

Disons que je donne les résultats suivants:Comment bande décorateurs d'une fonction en python

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

@with_connection 
def spam(connection): 
    # Do something 

Je veux tester la fonction spam sans passer par les tracas de la mise en place d'une connexion (ou quel que soit le décorateur est faire).

Étant donné spam, comment puis-je enlever le décorateur et obtenir la fonction "non décorée" sous-jacente?

Répondre

30

Dans le cas général, vous ne pouvez pas, parce que

@with_connection 
def spam(connection): 
    # Do something 

est équivalent à

def spam(connection): 
    # Do something 

spam = with_connection(spam) 

ce qui signifie que le spam "d'origine" pourrait même pas exister plus. A (pas trop jolie) pirater serait ceci:

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

Si vous allez modifier le code pour appeler '_original', vous pouvez aussi commenter le décorateur. – eduffy

+2

@eduffy: C'est le but de la question. – balpha

+0

Vous avez raison ... Je n'y ai pas réfléchi. – eduffy

14

Voici, FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:

orig_spam = spam.func_closure[0].cell_contents 

Modifier: Pour les fonctions/méthodes décorées plus d'une fois et décorateurs plus complexes, vous pouvez essayer d'utiliser le code suivant. Il repose sur le fait que les fonctions décorées sont __name__d différemment de la fonction d'origine.

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> 

Ce n'est pas infaillible cependant. Cela échouera si le nom de la fonction renvoyée par un décorateur est le même que celui qui a été décoré. L'ordre des contrôles hasattr() est aussi une heuristique, il y a des chaînes de décoration qui retournent des résultats erronés dans tous les cas.

+3

'func_closure' est remplacé par' __closure__' dans 3.x et c'est déjà dans 2.6 –

+1

J'ai vu ça quand je jouais avec des fonctions, mais ça devient compliqué si vous utilisez plus d'un décorateur pour une fonction. Vous finissez par appeler '.func_closure [0] .cell_contents' jusqu'à ce que' cell_contents ait None'. J'espérais une solution plus élégante. – Herge

+0

Probablement ne fonctionnera pas, si le décorateur utilise functools.wraps –

27

solution de balpha peut être plus généralisables avec cette méta-décorateur:

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

Ensuite, vous pouvez décorer vos décorateurs avec @include_original, et chacun aura une version testable (undecorated) caché à l'intérieur.

@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

Maintenant, nous parlons. métadécoration FTW. – brice

+1

Existe-t-il un moyen d'étendre ceci afin que l'original de niveau le plus profond soit accessible à la fonction décorée la plus externe, donc je n'ai pas besoin de faire ._original._original._original pour une fonction enveloppée dans trois décorateurs? – Sparr

+0

@jcdyer Que signifient exactement __ décorer vos décorateurs? Puis-je faire quelque chose comme \ @include_original (ligne suivante) \ @decorator_which_I_dont_control (ligne suivante) function_definition? – Harshdeep

2

L'approche habituelle pour tester de telles fonctions consiste à rendre les dépendances, telles que get_connection, configurables. Ensuite, vous pouvez le remplacer par un simulacre pendant le test. Fondamentalement la même chose que l'injection de dépendance dans le monde Java mais beaucoup plus simple grâce à la nature dynamique de Python.

Code pour qu'il pourrait ressembler à ceci:

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

En fonction de votre code, vous pouvez trouver un meilleur objet que le décorateur pour coller la fonction d'usine sur. Le problème avec l'avoir sur le décorateur est que vous devriez vous rappeler de le restaurer à l'ancienne valeur dans la méthode de démontage.

6

Au lieu de faire ..

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) 

Vous pouvez tout simplement faire ..

def with_connection(f): 
    .... 

def spam_f(connection): 
    ... 

spam = with_connection(spam_f) 

..qui est tout ce que la syntaxe @decorator fait - vous pouvez accéder à l'évidence la spam_f originale normalement

+0

Belle approche, si malin! – laike9m

1

Ajouter un décorateur do-rien:

def do_nothing(f): 
    return f 

Après avoir défini ou l'importation with_connection mais avant d'arriver à des méthodes qui utilisent comme décorateur, ajouter:

if TESTING: 
    with_connection = do_nothing 

Ensuite, si vous définissez l'essai global sur True, vous avez remplacé with_connection avec un décorateur qui ne fait rien.

16

Il y a eu un peu de mise à jour pour cette question. Si vous utilisez Python 3, vous pouvez utiliser la propriété __wrapped__ qui renvoie la fonction encapsulée.

Voici un exemple de Python Cookbook, 3rd edition

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

Voir la discussion pour une utilisation plus détaillée de cet attribut.

+0

Python3 pour la victoire! – funk

4

Vous pouvez maintenant utiliser le package undecorated:

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

Il passe par les tracas de creuser à travers toutes les couches de décorateurs différents jusqu'à ce qu'il atteigne la fonction fond et ne nécessite pas de changer les décorateurs d'origine. Fonctionne sur python2 et python3.

Questions connexes