了解使基于类的装饰器支持实例方法的技术[重复]

Posted

技术标签:

【中文标题】了解使基于类的装饰器支持实例方法的技术[重复]【英文标题】:Understanding a technique to make class-based decorators support instance methods [duplicate] 【发布时间】:2018-08-05 13:47:31 【问题描述】:

我最近在 Python 装饰器库的 memoized 装饰器中发现了一种技术,它允许它支持实例方法:

import collections
import functools


class memoized(object):
    '''Decorator. Caches a function's return value each time it is called.
    If called later with the same arguments, the cached value is returned
    (not reevaluated).
    '''
    def __init__(self, func):
        self.func = func
        self.cache = 

    def __call__(self, *args):
        if not isinstance(args, collections.Hashable):
        # uncacheable. a list, for instance.
        # better to not cache than blow up.
            return self.func(*args)
        if args in self.cache:
            return self.cache[args]
        else:
            value = self.func(*args)
            self.cache[args] = value
            return value

    def __repr__(self):
        '''Return the function's docstring.'''
        return self.func.__doc__

    def __get__(self, obj, objtype):
        '''Support instance methods.'''
        return functools.partial(self.__call__, obj)

__get__ 方法是,如 doc 字符串中所述,“魔法发生”使装饰器支持实例方法。以下是一些表明它有效的测试:

import pytest

def test_memoized_function():
    @memoized
    def fibonacci(n):
        "Return the nth fibonacci number."
        if n in (0, 1):
            return n
        return fibonacci(n-1) + fibonacci(n-2)

    assert fibonacci(12) == 144

def test_memoized_instance_method():
    class Dummy(object):
        @memoized
        def fibonacci(self, n):
            "Return the nth fibonacci number."
            if n in (0, 1):
                return n
            return self.fibonacci(n-1) + self.fibonacci(n-2)            

    assert Dummy().fibonacci(12) == 144

if __name__ == "__main__":
    pytest.main([__file__])

我想了解的是:这种技术究竟是如何工作的?它似乎非常普遍地适用于基于类的装饰器,我在对Is it possible to numpy.vectorize an instance method? 的回答中应用了它。

到目前为止,我已经通过注释掉__get__ 方法并在else 子句之后放入调试器来对此进行调查。 self.func 似乎是这样的,每当您尝试使用数字作为输入来调用它时,它都会引发 TypeError

> /Users/kurtpeek/Documents/Scratch/memoize_fibonacci.py(24)__call__()
     23                         import ipdb; ipdb.set_trace()
---> 24                         value = self.func(*args)
     25                         self.cache[args] = value

ipdb> self.func
<function Dummy.fibonacci at 0x10426f7b8>
ipdb> self.func(0)
*** TypeError: fibonacci() missing 1 required positional argument: 'n'

据我从https://docs.python.org/3/reference/datamodel.html#object.get 了解到,定义您自己的__get__ 方法会以某种方式覆盖您(在这种情况下)调用self.func 时发生的情况,但我很难将抽象文档与此示例联系起来。谁能一步一步解释这个?

【问题讨论】:

【参考方案1】:

据我所知,当你使用描述符来装饰一个实例方法(实际上是一个属性)时,它定义了如何setgetdelete 这个属性的行为。有一个ref。

所以在你的例子中,memoized__get__ 定义了如何获取属性fibonacci。在__get__ 中,它将obj 传递给self.__call__,其中obj 是实例。而支持实例方法的关键是填写参数self

所以流程是:

假设存在Dummy 的实例dummy。当您访问dummy 的属性fibonacci 时,它已被memoized 修饰。属性fibonacci 的值由memoized.__get__ 返回。 __get__ 接受两个参数,一个是调用实例(这里是dummy),另一个是它的类型。 memoized.__get__ 将实例填充到 self.__call__ 中,以便在原始方法 fibonacci 中填充 self 参数。

为了更好地理解描述符,有一个example:

class RevealAccess(object):
    """A data descriptor that sets and returns values
       normally and prints a message logging their access.
    """

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print('Retrieving', self.name)
        return self.val

    def __set__(self, obj, val):
        print('Updating', self.name)
        self.val = val

>>> class MyClass(object):
...     x = RevealAccess(10, 'var "x"')
...     y = 5
...
>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5

【讨论】:

以上是关于了解使基于类的装饰器支持实例方法的技术[重复]的主要内容,如果未能解决你的问题,请参考以下文章

装饰器、装饰器类与类装饰器(三)

类的单实例模式

Python-类的其他成员反射

打字稿装饰器与类继承

python 面向对象与装饰器

ES6 - 装饰器 - Decorater