Понимание метода, позволяющего декораторам на основе классов поддерживать методы экземпляра

Недавно я наткнулся на прием в декораторе memoized библиотеки декораторов Python, который позволяет ему поддерживать методы экземпляра:

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__, как объяснено в строке документа, где «происходит волшебство», чтобы декоратор поддерживал методы экземпляра. Вот несколько тестов, показывающих, что это работает:

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

Я пытаюсь понять: как именно работает эта техника? Кажется, что это вполне применимо к декораторам на основе классов, и я применил его в своем ответе на Можно ли numpy.vectorize метод экземпляра?.

До сих пор я исследовал это, закомментировав метод __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, но я изо всех сил пытаюсь связать абстрактную документацию к этому примеру. Кто-нибудь может объяснить это пошагово?


person Kurt Peek    schedule 26.02.2018    source источник


Ответы (1)


Насколько я могу судить, когда вы используете дескриптор для украшения метода экземпляра (на самом деле, атрибута), он определяет поведение того, как set, get и delete этот атрибут. Существует ссылка.

Итак, в вашем примере memoized __get__ определяет, как получить атрибут fibonacci. В __get__ он передает obj в self.__call__, где obj является экземпляром. И ключом к поддержке метода экземпляра является заполнение аргумента self.

Итак, процесс:

Предположим, что существует экземпляр dummy из Dummy. Когда вы получаете доступ к атрибуту fibonacci dummy, так как он был украшен memoized. Значение атрибута fibonacci возвращается функцией memoized.__get__. __get__ принимает два аргумента, один из которых является вызывающим экземпляром (здесь dummy), а другой — его типом. memoized.__get__ заполните экземпляр в self.__call__, чтобы заполнить аргумент self внутри оригинального метода fibonacci.

Чтобы хорошо понять дескриптор, есть пример:

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
person Sraw    schedule 26.02.2018