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