Recherche…


Introduction

Les fonctions de décorateur sont des modèles de conception de logiciel. Ils modifient dynamiquement les fonctionnalités d'une fonction, d'une méthode ou d'une classe sans avoir à utiliser directement les sous-classes ou à modifier le code source de la fonction décorée. Utilisés correctement, les décorateurs peuvent devenir des outils puissants dans le processus de développement. Cette rubrique couvre l'implémentation et les applications des fonctions de décorateur dans Python.

Syntaxe

  • def decorator_function (f): pass # définit un décorateur nommé decorator_function

  • @decorator_function
    def decorated_function (): pass # la fonction est maintenant encapsulée (décorée par)

  • decorated_function = decorator_function (decorated_function) # équivaut à utiliser le sucre syntaxique @decorator_function

Paramètres

Paramètre Détails
F La fonction à décorer

Fonction de décorateur

Les décorateurs augmentent le comportement d'autres fonctions ou méthodes. Toute fonction prenant une fonction en paramètre et renvoyant une fonction augmentée peut être utilisée comme décorateur .

# This simplest decorator does nothing to the function being decorated. Such
# minimal decorators can occasionally be used as a kind of code markers.
def super_secret_function(f):
    return f

@super_secret_function
def my_function():
    print("This is my secret function.")

La @ -notation est un sucre syntaxique équivalent à ce qui suit:

my_function = super_secret_function(my_function)

Il est important de garder cela à l’esprit pour comprendre le fonctionnement des décorateurs. Cette syntaxe "unsugared" indique clairement pourquoi la fonction de décorateur prend une fonction comme argument et pourquoi elle doit renvoyer une autre fonction. Cela montre également ce qui arriverait si vous ne retournez pas une fonction:

def disabled(f):
    """
    This function returns nothing, and hence removes the decorated function
    from the local scope.
    """
    pass

@disabled
def my_function():
    print("This function can no longer be called...")

my_function()
# TypeError: 'NoneType' object is not callable

Ainsi, nous définissons généralement une nouvelle fonction dans le décorateur et la retournons. Cette nouvelle fonction ferait d'abord quelque chose qu'il doit faire, puis appelle la fonction d'origine et traite enfin la valeur de retour. Considérons cette fonction de décorateur simple qui imprime les arguments reçus par la fonction d'origine, puis les appelle.

#This is the decorator
def print_args(func):
    def inner_func(*args, **kwargs):
        print(args)
        print(kwargs)
        return func(*args, **kwargs) #Call the original function with its arguments.
    return inner_func

@print_args
def multiply(num_a, num_b):
    return num_a * num_b
  
print(multiply(3, 5))
#Output:
# (3,5) - This is actually the 'args' that the function receives.
# {} - This is the 'kwargs', empty because we didn't specify keyword arguments.
# 15 - The result of the function.

Classe de décorateur

Comme mentionné dans l'introduction, un décorateur est une fonction qui peut être appliquée à une autre fonction pour augmenter son comportement. Le sucre syntaxique est équivalent à ce qui suit: my_func = decorator(my_func) . Mais si le decorator était plutôt une classe? La syntaxe fonctionne toujours, sauf que maintenant my_func est remplacé par une instance de la classe de decorator . Si cette classe implémente la méthode magique __call__() , alors il serait toujours possible d'utiliser my_func comme si c'était une fonction:

class Decorator(object):
    """Simple decorator class."""

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('Before the function call.')
        res = self.func(*args, **kwargs)
        print('After the function call.')
        return res

@Decorator
def testfunc():
    print('Inside the function.')

testfunc()
# Before the function call.
# Inside the function.
# After the function call.

Notez qu'une fonction décorée avec un décorateur de classe ne sera plus considérée comme une "fonction" du point de vue de la vérification de type:

import types
isinstance(testfunc, types.FunctionType)
# False
type(testfunc)
# <class '__main__.Decorator'>

Méthodes de décoration

Pour les méthodes de décoration, vous devez définir une __get__ __get__ supplémentaire:

from types import MethodType

class Decorator(object):
    def __init__(self, func):
        self.func = func
        
    def __call__(self, *args, **kwargs):
        print('Inside the decorator.')
        return self.func(*args, **kwargs)
    
    def __get__(self, instance, cls):
        # Return a Method if it is called on an instance
        return self if instance is None else MethodType(self, instance)

class Test(object):
    @Decorator
    def __init__(self):
        pass
    
a = Test()

À l'intérieur du décorateur.

Attention!

Les décorateurs de classes ne produisent qu'une seule instance pour une fonction spécifique. Ainsi, décorer une méthode avec un décorateur de classes partagera le même décorateur entre toutes les instances de cette classe:

from types import MethodType

class CountCallsDecorator(object):
    def __init__(self, func):
        self.func = func
        self.ncalls = 0    # Number of calls of this method
        
    def __call__(self, *args, **kwargs):
        self.ncalls += 1   # Increment the calls counter
        return self.func(*args, **kwargs)
    
    def __get__(self, instance, cls):
        return self if instance is None else MethodType(self, instance)

class Test(object):
    def __init__(self):
        pass
    
    @CountCallsDecorator
    def do_something(self):
        return 'something was done'
    
a = Test()
a.do_something()
a.do_something.ncalls   # 1
b = Test()
b.do_something()
b.do_something.ncalls   # 2

Faire ressembler un décorateur à la fonction décorée

Les décorateurs éliminent normalement les métadonnées des fonctions car elles ne sont pas identiques. Cela peut entraîner des problèmes lors de l'utilisation de la méta-programmation pour accéder de manière dynamique aux métadonnées de la fonction. Les métadonnées incluent également les docstrings de la fonction et son nom. functools.wraps permet à la fonction décorée de ressembler à la fonction d'origine en copiant plusieurs attributs dans la fonction wrapper.

from functools import wraps

Les deux méthodes d'emballage d'un décorateur permettent d'obtenir la même chose en masquant la fonction d'origine. Il n'y a aucune raison de préférer la version de la fonction à la version de la classe, sauf si vous l'utilisez déjà.

En tant que fonction

def decorator(func):
    # Copies the docstring, name, annotations and module to the decorator
    @wraps(func)
    def wrapped_func(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapped_func

@decorator
def test():
    pass

test.__name__

'tester'

En tant que classe

class Decorator(object):
    def __init__(self, func):
        # Copies name, module, annotations and docstring to the instance.
        self._wrapped = wraps(func)(self)
        
    def __call__(self, *args, **kwargs):
        return self._wrapped(*args, **kwargs)

@Decorator
def test():
    """Docstring of test."""
    pass

test.__doc__

'Docstring de test.'

Décorateur avec des arguments (usine de décorateur)

Un décorateur ne prend qu'un argument: la fonction à décorer. Il n'y a aucun moyen de passer d'autres arguments.

Mais des arguments supplémentaires sont souvent souhaités. L'astuce consiste alors à créer une fonction qui prend des arguments arbitraires et renvoie un décorateur.

Fonctions de décorateur

def decoratorfactory(message):
    def decorator(func):
        def wrapped_func(*args, **kwargs):
            print('The decorator wants to tell you: {}'.format(message))
            return func(*args, **kwargs)
        return wrapped_func
    return decorator

@decoratorfactory('Hello World')
def test():
    pass

test()

Le décorateur veut vous dire: Hello World

Note importante:

Avec de telles usines de décoration, vous devez appeler le décorateur avec une paire de parenthèses:

@decoratorfactory # Without parentheses
def test():
    pass

test()

TypeError: decorator () manquant 1 argument positionnel requis: 'func'

Cours de décorateur

def decoratorfactory(*decorator_args, **decorator_kwargs):
    
    class Decorator(object):
        def __init__(self, func):
            self.func = func

        def __call__(self, *args, **kwargs):
            print('Inside the decorator with arguments {}'.format(decorator_args))
            return self.func(*args, **kwargs)
        
    return Decorator

@decoratorfactory(10)
def test():
    pass

test()

À l'intérieur du décorateur avec des arguments (10,)

Créer une classe singleton avec un décorateur

Un singleton est un modèle qui limite l'instanciation d'une classe à une instance / objet. En utilisant un décorateur, nous pouvons définir une classe en tant que singleton en forçant la classe à renvoyer une instance existante de la classe ou à créer une nouvelle instance (si elle n'existe pas).

def singleton(cls):    
    instance = [None]
    def wrapper(*args, **kwargs):
        if instance[0] is None:
            instance[0] = cls(*args, **kwargs)
        return instance[0]

    return wrapper

Ce décorateur peut être ajouté à toute déclaration de classe et s'assurera qu'au plus une instance de la classe soit créée. Tous les appels suivants renverront l'instance de classe déjà existante.

@singleton
class SomeSingletonClass:
    x = 2
    def __init__(self):
        print("Created!")

instance = SomeSingletonClass()  # prints: Created!
instance = SomeSingletonClass()  # doesn't print anything
print(instance.x)                # 2

instance.x = 3
print(SomeSingletonClass().x)    # 3

Peu importe que vous vous référiez à l'instance de classe via votre variable locale ou que vous créiez une autre "instance", vous obtenez toujours le même objet.

Utiliser un décorateur pour chronométrer une fonction

import time
def timer(func):
    def inner(*args, **kwargs):
        t1 = time.time()
        f = func(*args, **kwargs)
        t2 = time.time()
        print 'Runtime took {0} seconds'.format(t2-t1)
        return f
    return inner

@timer
def example_function():
    #do stuff


example_function()


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow