相当于类的Python functools.wraps


76

当使用一个类中定义一个装饰,我怎么自动转移__name____module____doc__?通常,我会使用functools中的@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 __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方法自动化(我想这是为了创建绑定方法?)是否缺少任何方法?

Answers:


57

每个人似乎都错过了显而易见的解决方案。

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

13
__name____doc__设置在实例,而不是类,这是始终使用help(instance)。要修复,不能使用基于类的装饰器实现,而必须将装饰器实现为一个函数。有关详细信息,请参见stackoverflow.com/a/25973438/1988505
Wesley Baugh 2014年

2
我不确定为什么昨天突然将我的答案记下来了。没有人问过要获得help()的工作。在3.5中,inspect.signature()和inspect.from_callable()得到了一个新的follow_wrapped选项;也许help()应该做同样的事情?
samwyse,2017年

幸运的是,ipythonfibonacci?的确显示了包装程序中的文档以及已记录的类,因此您同时获得了两者
vdboor

这不会产生可
腌制的

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)

谢谢mouad。您知道该__get__方法的目的是什么?
尼尔·G

哦,我明白了:它使装饰器与方法一起工作?那可能应该在class_wraps中吗?
尼尔·G

1
@Neil:是有关更多详细信息,请访问:stackoverflow.com/questions/5469956/…,IMO,我不这么认为,因为这将违反我认为对函数或类具有唯一责任的原则之一,在这种情况下的class_wraps将是更新的包装类,看起来像包裹。不多不多:)
mouad 2011年

1
@mouad:非常感谢。如果您不介意,我还有其他几个问题(对您或其他任何人):1.我们是否要__get__为所有“可调用的类”修饰符覆盖是真的吗?2.为什么我们使用functools.partial而不是返回带有的绑定方法types.MethodType(self.__call__, obj)
尼尔·G

@Neil:1.是的,如果您希望能够像已经说过的那样也装饰方法(不仅仅是函数),并且我坚信_get__为类装饰器也实现该方法是一种好习惯,这样就不会出现任何奇怪的问题之后:) 2.我认为这只是一个优先权问题the beauty is in the eye of the beholder,我更喜欢functools.partial在这种情况下使用,主要是我types.*用来测试对象的类型,希望我能回答您的问题:)
mouad 2011年

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

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属性的代码。Mouad的解决方案虽然有效。报告的问题是: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();您应该用google搜索method resolution order python)。
ninjagecko

@ninjagecko:不是由超类调用__init__其他父类的方法吗?
尼尔·G

据我所知,这有些悬而未决,但我可能是错的。fuhm.net/super-harmful另外stackoverflow.com/questions/1385759/…似乎并未表明任何共识。
ninjagecko 2011年

1
@ninjagecko:是的,我已经阅读了第一篇文章。我一直在做什么,无论如何,总是从每个类中调用super().__ init__。这样__init__,只要我继承的每个人都可以执行此操作,就可以依靠所有被调用的方法。不幸的是,我发现PyQt类不能做到这一点。我真的以为这就是合作继承的工作方式,但是从您的意思看来,我可能是唯一的继承人!
尼尔·G
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.