Python functools.wraps, еквівалентний для класів


76

Як визначати декоратора за допомогою класу, як я автоматично переношу текст __name__, __module__і __doc__? Зазвичай я б використовував декоратор @wraps від functools. Ось що я зробив замість цього для класу (це не зовсім мій код):

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

Чи існує стандартний декоратор для автоматизації створення модуля імен та документа? Крім того, для автоматизації методу get (я припускаю, що це для створення зв’язаних методів?) Чи відсутні відсутні методи?

Відповіді:


59

Здається, всі пропустили очевидне рішення.

>>> 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'

15
Значення __name__і __doc__встановлюються в екземплярі , але не в класі, який завжди використовується help(instance). Для виправлення не можна використовувати реалізацію декоратора на основі класу, і замість цього декоратор повинен бути реалізований як функція. Докладніше див. Na stackoverflow.com/a/25973438/1988505 .
Wesley Baugh

2
Я не впевнений, чому вчора мою відповідь раптом позначили. Ніхто не запитував про отримання допомоги () для роботи. У версії 3.5 inspect.signature () та inspect.from_callable () отримали нову опцію follow_wrapped; можливо, help () повинен зробити те саме?
samwyse

На щастя, ipython fibonacci?показує як документ із обгортки, так і пам’ятний клас, тож ви отримуєте обидва
vdboor

Це не призводить до оформлення декораторів класу
mathtick

25

Я не знаю таких речей у stdlib, але ми можемо створити свої власні, якщо нам потрібно.

Щось подібне може працювати:

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

Використання:

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

Вихід:

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

РЕДАГУВАТИ:

І якщо вам цікаво, чому це не було включено в stdlib, це тому, що ви можете обернути свій декоратор класу в декоратор функцій і використовувати functools.wrapsтак:

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)

Дякую, муад. Чи знаєте ви, яке призначення __get__методу?
Neil G

О, я бачу: це змушує декоратора працювати методами? Тоді це, мабуть, має бути в class_wraps?
Neil G

1
@Neil: Так Більш детально: stackoverflow.com/questions/5469956/… , ІМО, я не думаю, тому що це порушить один із принципів, у який я вірю, щодо функції чи класу, що є унікальною відповідальністю , що у випадку of class_wrapsбуде оновити клас обгортки, щоб виглядати як обгорнутий. не менше не більше :)
mouad

1
@mouad: Велике спасибі. У мене є ще кілька запитань (до вас чи когось іншого), якщо ви не проти: 1. Чи не правда, що ми хочемо замінити __get__всі декоратори "викличного класу"? 2. Чому ми використовуємо functools.partialзамість того, щоб повернути пов'язаний метод за допомогою types.MethodType(self.__call__, obj)?
Neil G

@Neil: 1. Так, якщо ви хочете мати можливість декорувати також методи (а не лише функції), як ви вже сказали, і я твердо переконаний, що це гарна практика застосовувати також _get__метод для декоратора класів, щоб не виникало дивних проблем після :) 2. Я думаю, що це просто питання переваги the beauty is in the eye of the beholder, я вважаю за краще використовувати functools.partialтакі випадки, і в основному я використовую types.*для тестування типів об'єкта, сподіваюся, я відповім на ваші запитання :)
mouad

4

Мені потрібно було щось, що б обгорнуло як класи, так і функції, і написав це:

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

1
Побічна примітка, я запрошую всіх використовувати цю .is_timeout=Trueідіому, щоб позначити ваші очікування, спричинені помилками, та прийняти цей API з інших пакетів.
temoto

1

Все, що нам дійсно потрібно зробити, це змінити поведінку декоратора таким чином, щоб воно було "гігієнічним", тобто воно зберігало атрибути.

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

Це ВСЕ, що вам потрібно. Загалом. Він не зберігає підпис, але якщо ви дійсно хочете, для цього ви можете скористатися бібліотекою. Я також продовжив і переписав код мемоїзації, щоб він працював і на аргументах ключових слів. Також виникла помилка, коли відмова перетворити її на хешируваний кортеж змусила б її не працювати у 100% випадків.

Демонстрація переписаного memoizedдекоратора із @hygienicзміною його поведінки. memoizedтепер є функцією, яка обгортає початковий клас, хоча ви можете, як і інша відповідь, написати клас обтікання замість цього, або навіть краще, щось, що виявляє, чи це клас, і якщо так обертає __init__метод.

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

У дії:

@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!'

@Hygienic не працює для коду, де обгорнутий клас декоратора має атрибут class. Однак рішення Муада працює. Повідомляється про проблему: AttributeError: 'function' object has no attribute 'level'при спробі зробити decoratorclassname.level += 1всередині__call__
cfi

0

Інше рішення з використанням успадкування:

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)

І, використання:

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)

Причина, по якій ви не повинні використовувати це, полягає в тому, що, як ви показуєте, вам потрібно викликати __init__метод батьківських класів (не обов'язково просто super(); вам слід погуглити method resolution order python).
ninjagecko

@ninjagecko: Хіба не супер класу викликати __init__метод інших батьківських класів?
Neil G

Це дещо відкрите питання, наскільки я знаю, я, можливо, помиляюся. fuhm.net/super-harmful Також stackoverflow.com/questions/1385759 / ... , здається, не показують будь - яких консенсусу.
ninjagecko

1
@ninjagecko: Так, я прочитав першу статтю. Те, що я робив, це завжди викликати super () .__ init__ з кожного класу, незважаючи ні на що. Таким чином, я можу розраховувати на те, що всі __init__методи будуть викликані, доки всі, кого я успадковую, роблять це. На жаль, я виявив, що класи PyQt цього не роблять. Я справді думав, що так мало працювати кооперативне успадкування, але з того, що ви кажете, це звучить так, ніби я міг бути єдиним!
Neil G

0

Виявляється, існує просте рішення, яке використовує functools.wrapsсаме себе:

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'>

Зверніть увагу, що updated=()це потрібно для запобігання спробі оновлення класу __dict__(цей результат не має 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'

Я не розумію, яке відношення це має до мого запитання. Як реалізувати memoizedяк клас і надати функцію обтікання?
Neil G
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.