80 votes

équivalent Python functools.wraps pour les classes

Lorsque l'on définit un décorateur à l'aide d'une classe, comment faire pour transférer automatiquement le __name__ , __module__ y __doc__ ? Normalement, je devrais utiliser le décorateur @wraps de functools. Voici ce que j'ai fait à la place pour une classe (ce n'est pas entièrement mon code) :

class memoized:
    """Decorator that caches a function's return value each time it is called.
    If called later with the same arguments, the cached value is returned, and
    not re-evaluated.
    """
    def __init__(self, func):
        super().__init__()
        self.func = func
        self.cache = {}

    def __call__(self, *args):
        try:
            return self.cache[args]
        except KeyError:
            value = self.func(*args)
            self.cache[args] = value
            return value
        except TypeError:
            # uncacheable -- for instance, passing a list as an argument.
            # Better to not cache than to blow up entirely.
            return self.func(*args)

    def __repr__(self):
        return self.func.__repr__()

    def __get__(self, obj, objtype):
        return functools.partial(self.__call__, obj)

    __doc__ = property(lambda self:self.func.__doc__)
    __module__ = property(lambda self:self.func.__module__)
    __name__ = property(lambda self:self.func.__name__)

Existe-t-il un décorateur standard pour automatiser la création du module de nom et de la doc ? Aussi, pour automatiser la méthode get (je suppose que c'est pour créer des méthodes liées ?) Y a-t-il des méthodes manquantes ?

61voto

samwyse Points 435

Tout le monde semble avoir manqué la solution évidente.

>>> import functools
>>> class memoized(object):
    """Decorator that caches a function's return value each time it is called.
    If called later with the same arguments, the cached value is returned, and
    not re-evaluated.
    """
    def __init__(self, func):
        self.func = func
        self.cache = {}
        functools.update_wrapper(self, func)  ## TA-DA! ##
    def __call__(self, *args):
        pass  # Not needed for this demo.

>>> @memoized
def fibonacci(n):
    """fibonacci docstring"""
    pass  # Not needed for this demo.

>>> fibonacci
<__main__.memoized object at 0x0156DE30>
>>> fibonacci.__name__
'fibonacci'
>>> fibonacci.__doc__
'fibonacci docstring'

17 votes

Le site __name__ y __doc__ sont fixés sur le instance mais pas la classe, ce qui est toujours utilisé par help(instance) . Pour corriger cela, une implémentation de décorateur basée sur une classe ne peut pas être utilisée, et à la place le décorateur doit être implémenté comme une fonction. Pour plus de détails, voir stackoverflow.com/a/25973438/1988505 .

2 votes

Je ne sais pas pourquoi ma réponse a été soudainement annulée hier. Personne n'a demandé comment faire fonctionner help(). En 3.5, inspect.signature() et inspect.from_callable() ont une nouvelle option follow_wrapped ; peut-être que help() devrait faire de même ?

0 votes

Heureusement, la fonction fibonacci? montre à la fois le doc de l'enveloppe, et la classe mémorisée afin que vous obteniez à la fois

25voto

mouad Points 21520

Je n'ai pas connaissance de telles choses dans stdlib, mais nous pouvons créer les nôtres si nous en avons besoin.

Quelque chose comme cela peut fonctionner :

from functools import WRAPPER_ASSIGNMENTS

def class_wraps(cls):
    """Update a wrapper class `cls` to look like the wrapped."""

    class Wrapper(cls):
        """New wrapper that will extend the wrapper `cls` to make it look like `wrapped`.

        wrapped: Original function or class that is beign decorated.
        assigned: A list of attribute to assign to the the wrapper, by default they are:
             ['__doc__', '__name__', '__module__', '__annotations__'].

        """

        def __init__(self, wrapped, assigned=WRAPPER_ASSIGNMENTS):
            self.__wrapped = wrapped
            for attr in assigned:
                setattr(self, attr, getattr(wrapped, attr))

            super().__init__(wrapped)

        def __repr__(self):
            return repr(self.__wrapped)

    return Wrapper

Utilisation :

@class_wraps
class memoized:
    """Decorator that caches a function's return value each time it is called.
    If called later with the same arguments, the cached value is returned, and
    not re-evaluated.
    """

    def __init__(self, func):
        super().__init__()
        self.func = func
        self.cache = {}

    def __call__(self, *args):
        try:
            return self.cache[args]
        except KeyError:
            value = self.func(*args)
            self.cache[args] = value
            return value
        except TypeError:
            # uncacheable -- for instance, passing a list as an argument.
            # Better to not cache than to blow up entirely.
            return self.func(*args)

    def __get__(self, obj, objtype):
        return functools.partial(self.__call__, obj)

@memoized
def fibonacci(n):
    """fibonacci docstring"""
    if n in (0, 1):
       return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci)
print("__doc__: ", fibonacci.__doc__)
print("__name__: ", fibonacci.__name__)

Sortie :

<function fibonacci at 0x14627c0>
__doc__:  fibonacci docstring
__name__:  fibonacci

EDITAR:

Et si vous vous demandez pourquoi cela n'a pas été inclus dans la stdlib, c'est parce que vous pouvez envelopper votre décorateur de classe dans un décorateur de fonction et utiliser functools.wraps comme ça :

def wrapper(f):

    memoize = memoized(f)

    @functools.wraps(f)
    def helper(*args, **kws):
        return memoize(*args, **kws)

    return helper

@wrapper
def fibonacci(n):
    """fibonacci docstring"""
    if n <= 1:
       return n
    return fibonacci(n-1) + fibonacci(n-2)

0 votes

Merci mouad. Savez-vous à quoi sert le __get__ La méthode est ?

0 votes

Oh, je vois : cela fait fonctionner le décorateur avec des méthodes ? Il devrait probablement être dans class_wraps alors ?

1 votes

@Neil : Oui Pour plus de détails : stackoverflow.com/questions/5469956/ Je ne pense pas que ce soit le cas, car cela violerait l'un des principes auxquels je crois pour la fonction ou la classe, qui est le suivant responsabilité unique qui, dans le cas de class_wraps sera de Mettre à jour une classe wrapper pour qu'elle ressemble à la classe wrapped. pas moins, pas plus :)

4voto

temoto Points 1323

J'avais besoin de quelque chose qui puisse envelopper à la fois les classes et les fonctions et j'ai écrit ceci :

def wrap_is_timeout(base):
    '''Adds `.is_timeout=True` attribute to objects returned by `base()`.

    When `base` is class, it returns a subclass with same name and adds read-only property.
    Otherwise, it returns a function that sets `.is_timeout` attribute on result of `base()` call.

    Wrappers make best effort to be transparent.
    '''
    if inspect.isclass(base):
        class wrapped(base):
            is_timeout = property(lambda _: True)

        for k in functools.WRAPPER_ASSIGNMENTS:
            v = getattr(base, k, _MISSING)
            if v is not _MISSING:
                try:
                    setattr(wrapped, k, v)
                except AttributeError:
                    pass
        return wrapped

    @functools.wraps(base)
    def fun(*args, **kwargs):
        ex = base(*args, **kwargs)
        ex.is_timeout = True
        return ex
    return fun

4voto

Anthony Sottile Points 3629

Il s'avère qu'il y a une solution simple qui consiste à utiliser functools.wraps lui-même :

import functools

def dec(cls):
    @functools.wraps(cls, updated=())
    class D(cls):
        decorated = 1
    return D

@dec
class C:
    """doc"""

print(f'{C.__name__=} {C.__doc__=} {C.__wrapped__=}')

$ python3 t.py 
C.__name__='C' C.__doc__='doc' C.__wrapped__=<class '__main__.C'>

Notez que updated=() est nécessaire pour empêcher une tentative de mise à jour de la classe __dict__ (cette sortie est sans updated=() ) :

$ python t.py
Traceback (most recent call last):
  File "t.py", line 26, in <module>
    class C:
  File "t.py", line 20, in dec
    class D(cls):
  File "/usr/lib/python3.8/functools.py", line 57, in update_wrapper
    getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
AttributeError: 'mappingproxy' object has no attribute 'update'

1voto

ninjagecko Points 25709

Tout ce que nous devons faire, c'est modifier le comportement du décorateur pour qu'il soit "hygiénique", c'est-à-dire qu'il préserve les attributs.

#!/usr/bin/python3

def hygienic(decorator):
    def new_decorator(original):
        wrapped = decorator(original)
        wrapped.__name__ = original.__name__
        wrapped.__doc__ = original.__doc__
        wrapped.__module__ = original.__module__
        return wrapped
    return new_decorator

C'est tout ce dont vous avez besoin. En général. Il ne préserve pas la signature, mais si vous le voulez vraiment, vous pouvez utiliser une bibliothèque pour le faire. J'ai également réécrit le code de mémorisation pour qu'il fonctionne également avec des arguments de type mot-clé. Il y avait également un bug où l'échec de la conversion en un tuple hachable ne fonctionnait pas dans 100% des cas.

Démonstration de la réécriture memoized décorateur avec @hygienic modifier son comportement. memoized est maintenant une fonction qui enveloppe la classe d'origine, bien que vous puissiez (comme l'autre réponse) écrire une classe enveloppante à la place, ou encore mieux, quelque chose qui détecte si c'est une classe et, si c'est le cas, enveloppe la fonction __init__ méthode.

@hygienic
class memoized:
    def __init__(self, func):
        self.func = func
        self.cache = {}

    def __call__(self, *args, **kw):
        try:
            key = (tuple(args), frozenset(kw.items()))
            if not key in self.cache:
                self.cache[key] = self.func(*args,**kw)
            return self.cache[key]
        except TypeError:
            # uncacheable -- for instance, passing a list as an argument.
            # Better to not cache than to blow up entirely.
            return self.func(*args,**kw)

En action :

@memoized
def f(a, b=5, *args, keyword=10):
    """Intact docstring!"""
    print('f was called!')
    return {'a':a, 'b':b, 'args':args, 'keyword':10}

x=f(0)  
#OUTPUT: f was called!
print(x)
#OUTPUT: {'a': 0, 'b': 5, 'keyword': 10, 'args': ()}                 

y=f(0)
#NO OUTPUT - MEANS MEMOIZATION IS WORKING
print(y)
#OUTPUT: {'a': 0, 'b': 5, 'keyword': 10, 'args': ()}          

print(f.__name__)
#OUTPUT: 'f'
print(f.__doc__)
#OUTPUT: 'Intact docstring!'

0 votes

Le @hygienic ne fonctionne pas pour le code où la classe du décorateur enveloppé a un attribut class. La solution de Mouad fonctionne cependant. Le problème signalé est le suivant : AttributeError: 'function' object has no attribute 'level' en essayant de faire decoratorclassname.level += 1 à l'intérieur de la __call__

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X