Python functools.wraps entspricht Klassen

76

Wenn ein Dekorateur mit einer Klasse definiert, wie transferiere ich automatisch über __name__, __module__und __doc__? Normalerweise würde ich den @wraps Dekorator von functools verwenden. Folgendes habe ich stattdessen für eine Klasse getan (dies ist nicht ganz mein 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__)

Gibt es einen Standarddekorateur, der die Erstellung von Namensmodulen und Dokumenten automatisiert? Auch um die get-Methode zu automatisieren (ich nehme an, das dient zum Erstellen gebundener Methoden?) Gibt es fehlende Methoden?

Neil G.
quelle

Antworten:

57

Jeder scheint die offensichtliche Lösung verpasst zu haben.

>>> 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'
Samwyse
quelle
13
Die __name__und __doc__werden auf die Instanz gesetzt , aber nicht auf die Klasse, die immer von verwendet wird help(instance). Um dies zu beheben, kann keine klassenbasierte Decorator-Implementierung verwendet werden. Stattdessen muss der Decorator als Funktion implementiert werden. Weitere Informationen finden Sie unter stackoverflow.com/a/25973438/1988505 .
Wesley Baugh
2
Ich bin mir nicht sicher, warum meine Antwort gestern plötzlich notiert wurde. Niemand fragte nach Hilfe (), um zu arbeiten. In 3.5 haben inspect.signature () und inspect.from_callable () eine neue Option follow_wrapped erhalten. Vielleicht sollte help () dasselbe tun?
Samwyse
Glücklicherweise zeigt ipython fibonacci?sowohl das Dokument aus dem Wrapper als auch die gespeicherte Klasse an, sodass Sie beide erhalten
vdboor
Dies führt nicht zu Picklable Class Decorators
Mathtick
25

Ich bin mir solcher Dinge in stdlib nicht bewusst, aber wir können unsere eigenen erstellen, wenn wir müssen.

So etwas kann funktionieren:

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

Verwendung:

@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__)

Ausgabe:

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

BEARBEITEN:

Und wenn Sie sich fragen, warum dies nicht in der stdlib enthalten war, können Sie Ihren Klassendekorator in einen Funktionsdekorator einwickeln und functools.wrapswie folgt verwenden :

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)
Mouad
quelle
Danke Mouad. Wissen Sie, was der Zweck der __get__Methode ist?
Neil G
Oh, ich verstehe: bringt es den Dekorateur dazu, mit Methoden zu arbeiten? Es sollte dann wahrscheinlich in class_wraps sein?
Neil G
1
@Neil: Ja Für weitere Details: stackoverflow.com/questions/5469956/… , IMO glaube ich nicht, weil es gegen eines der Prinzipien verstößt, an die ich glaube, für eine Funktion oder Klasse , die in diesem Fall eine einzigartige Verantwortung darstellt of class_wrapswird darin bestehen, eine Wrapper-Klasse so zu aktualisieren, dass sie wie die verpackte aussieht. nicht weniger nicht mehr :)
Mouad
1
@mouad: Vielen Dank. Ich habe noch ein paar Fragen (für Sie oder andere), wenn Sie nichts dagegen haben: 1. Stimmt es nicht, dass wir __get__alle Dekorateure der "Callable Class" außer Kraft setzen wollen ? 2. Warum verwenden wir, functools.partialanstatt eine gebundene Methode mit zurückzugeben types.MethodType(self.__call__, obj)?
Neil G
@Neil: 1. Ja, wenn Sie in der Lage sein möchten, auch Methoden (nicht nur Funktionen) zu dekorieren, wie Sie bereits gesagt haben, und ich bin der festen Überzeugung, dass es eine gute Praxis ist, auch die _get__Methode für Klassendekorateure zu implementieren , um keine seltsamen Probleme zu haben nach :) 2. Ich denke, es ist nur eine Frage der Präferenz the beauty is in the eye of the beholderrichtig, ich bevorzuge functools.partialin Fällen wie diesem und meistens verwende ich types.*, um die Arten eines Objekts zu testen. Ich hoffe, ich beantworte Ihre Fragen :)
Mouad
4

Ich brauchte etwas, das sowohl Klassen als auch Funktionen umschließt, und schrieb Folgendes:

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
temoto
quelle
1
Nebenbei bemerkt, ich lade alle ein, diese .is_timeout=TrueRedewendung zu verwenden, um Ihre durch Timeout verursachten Fehler zu markieren und diese API aus anderen Paketen zu akzeptieren.
Temoto
1

Alles, was wir wirklich tun müssen, ist, das Verhalten des Dekorateurs so zu ändern, dass es "hygienisch" ist, dh Attribute bewahrt.

#!/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

Das ist alles was du brauchst. Im Allgemeinen. Die Signatur wird nicht beibehalten, aber wenn Sie dies wirklich möchten, können Sie dazu eine Bibliothek verwenden. Ich habe auch den Memoization-Code neu geschrieben, damit er auch mit Schlüsselwortargumenten funktioniert. Es gab auch einen Fehler, bei dem das Nichtkonvertieren in ein Hash-Tupel dazu führte, dass es in 100% der Fälle nicht funktionierte.

Demo des umgeschriebenen memoizedDekorateurs mit @hygienicÄnderung seines Verhaltens. memoizedist jetzt eine Funktion, die die ursprüngliche Klasse umschließt, obwohl Sie (wie die andere Antwort) stattdessen eine Umbruchklasse schreiben können, oder noch besser, etwas, das erkennt, ob es sich um eine Klasse handelt, und wenn ja, die __init__Methode umschließt .

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

In Aktion:

@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!'
Ninjagecko
quelle
Das @hygienic funktioniert nicht für Code, bei dem die umschlossene Dekorationsklasse ein Klassenattribut hat. Mouads Lösung funktioniert jedoch. Das gemeldete Problem ist: AttributeError: 'function' object has no attribute 'level'beim Versuch, decoratorclassname.level += 1innerhalb des__call__
cfi
0

Eine andere Lösung mit Vererbung:

import functools
import types

class CallableClassDecorator:
    """Base class that extracts attributes and assigns them to self.

    By default the extracted attributes are:
         ['__doc__', '__name__', '__module__'].
    """

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

    def __get__(self, obj, objtype):
        return types.MethodType(self.__call__, obj)

Und Verwendung:

class memoized(CallableClassDecorator):
    """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, function):
        super().__init__(function)
        self.function = function
        self.cache = {}

    def __call__(self, *args):
        try:
            return self.cache[args]
        except KeyError:
            value = self.function(*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.function(*args)
Neil G.
quelle
Der Grund, warum Sie dies nicht verwenden sollten, ist, dass Sie, wie Sie zeigen, die __init__Methode der übergeordneten Klassen aufrufen müssen (nicht unbedingt nur super(); Sie sollten nach googeln method resolution order python).
Ninjagecko
@ninjagecko: Ist es nicht Sache der Superklasse, die __init__Methode der anderen übergeordneten Klassen aufzurufen ?
Neil G
Soweit ich weiß, ist es eine offene Frage, aber ich kann mich irren. fuhm.net/super-harmful Auch stackoverflow.com/questions/1385759/… scheint keinen Konsens anzuzeigen.
Ninjagecko
1
@ninjagecko: Ja, ich habe den ersten Artikel gelesen. Was ich getan habe, ist immer super () .__ init__ aus jeder Klasse aufzurufen, egal was passiert. Auf diese Weise kann ich mich darauf verlassen, dass alle __init__Methoden aufgerufen werden, solange dies jeder tut, von dem ich erbe. Leider habe ich festgestellt, dass PyQt-Klassen dies nicht tun. Ich dachte wirklich, dass kooperative Vererbung so funktionieren muss, aber nach dem, was Sie sagen, klingt es so, als wäre ich der einzige!
Neil G