Понимание техники, позволяющей декораторам на основе классов поддерживать методы экземпляров
Недавно я столкнулся с техникой в библиотеке декоратора 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__
Метод, как объяснено в строке документа, где "происходит волшебство", заставляет декоратор поддерживать методы экземпляра. Вот несколько тестов, показывающих, что это работает:
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 получить, определяя свой собственный __get__
метод каким-то образом отменяет то, что происходит, когда вы (в этом случае) вызываете self.func
, но я пытаюсь связать абстрактную документацию с этим примером. Кто-нибудь может объяснить это шаг за шагом?
1 ответ
Насколько я могу сказать, когда вы используете дескриптор для украшения метода экземпляра (на самом деле, атрибут), он определяет поведение того, как set
, get
а также delete
этот атрибут. Есть ссылка.
Итак, в вашем примере memoized
"s __get__
определяет, как получить атрибут fibonacci
, В __get__
Пройдите obj
в self.__call__
который obj
это экземпляр. И ключом к поддержке метода экземпляра является заполнение аргумента self
,
Итак, процесс таков:
Предположим, что есть экземпляр dummy
из Dummy
, Когда вы получаете доступ к dummy
атрибут fibonacci
, как это было украшено 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