Итак, я недавно задал вопрос о мемоизации и получил несколько отличных ответов, и теперь я хочу перейти на следующий уровень. Немного погуглив, я не смог найти эталонную реализацию декоратора memoize, который мог бы кэшировать функцию, принимающую аргументы ключевого слова.На самом деле, большинство из них просто использовали *args
в качестве ключа для поиска в кеше, а это означает, что он также сломается, если вы захотите запомнить функцию, которая принимает списки или словари в качестве аргументов.
В моем случае первый аргумент функции сам по себе является уникальным идентификатором, подходящим для использования в качестве ключа dict для поиска в кеше, однако я хотел иметь возможность использовать аргументы с ключевыми словами и по-прежнему получать доступ к тому же кешу. Я имею в виду, что my_func('unique_id', 10)
и my_func(foo=10, func_id='unique_id')
должны возвращать один и тот же кэшированный результат.
Чтобы сделать это, нам нужен чистый и питонический способ сказать: «проверить kwargs на предмет того ключевого слова, которое соответствует первому аргументу)». Вот что я придумал:
class memoize(object):
def __init__(self, cls):
if type(cls) is FunctionType:
# Let's just pretend that the function you gave us is a class.
cls.instances = {}
cls.__init__ = cls
self.cls = cls
self.__dict__.update(cls.__dict__)
def __call__(self, *args, **kwargs):
"""Return a cached instance of the appropriate class if it exists."""
# This is some dark magic we're using here, but it's how we discover
# that the first argument to Photograph.__init__ is 'filename', but the
# first argument to Camera.__init__ is 'camera_id' in a general way.
delta = 2 if type(self.cls) is FunctionType else 1
first_keyword_arg = [k
for k, v in inspect.getcallargs(
self.cls.__init__,
'self',
'first argument',
*['subsequent args'] * (len(args) + len(kwargs) - delta)).items()
if v == 'first argument'][0]
key = kwargs.get(first_keyword_arg) or args[0]
print key
if key not in self.cls.instances:
self.cls.instances[key] = self.cls(*args, **kwargs)
return self.cls.instances[key]
Сумасшествие в том, что это действительно работает. Например, если вы декорируете так:
@memoize
class FooBar:
instances = {}
def __init__(self, unique_id, irrelevant=None):
print id(self)
Тогда из вашего кода вы можете вызвать либо FooBar('12345', 20)
, либо FooBar(irrelevant=20, unique_id='12345')
и фактически получают один и тот же экземпляр FooBar. Затем вы можете определить другой класс с другим именем для первого аргумента, потому что он работает в общем виде (т. е. декоратору не нужно знать ничего конкретного о классе, который он украшает, чтобы это работало).
Проблема в том, что это безбожный беспорядок ;-)
Это работает, потому что inspect.getcallargs
возвращает словарь, сопоставляющий определенные ключевые слова с аргументами, которые вы ему предоставляете, поэтому я добавляю ему несколько фальшивых аргументов. а затем проверьте dict для первого переданного аргумента.
Что было бы намного лучше, если бы такая вещь вообще существовала, так это аналог inspect.getcallargs
, который возвращал бы оба типа аргументов, унифицированных как список аргументов, а не как словарь аргументы ключевого слова. Это позволило бы что-то вроде этого:
def __call__(self, *args, **kwargs):
key = inspect.getcallargsaslist(self.cls.__init__, None, *args, **kwargs)[1]
if key not in self.cls.instances:
self.cls.instances[key] = self.cls(*args, **kwargs)
return self.cls.instances[key]
Другим способом решения этой проблемы, который я вижу, было бы использование словаря, предоставленного inspect.getcallargs
, в качестве ключа кэша поиска, но это потребовало бы повторяемого способа создавать идентичные строки из одинаковых хэшей, на что, как я слышал, нельзя полагаться (я думаю, мне придется построить строку самостоятельно после сортировки ключей).
У кого-нибудь есть мысли по этому поводу? Неправильно ли вызывать функцию с ключевыми аргументами и кэшировать результаты? Или просто очень сложно?