Существует ли питонический способ поддержки аргументов ключевых слов для декоратора memoize в Python?

Итак, я недавно задал вопрос о мемоизации и получил несколько отличных ответов, и теперь я хочу перейти на следующий уровень. Немного погуглив, я не смог найти эталонную реализацию декоратора 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, в качестве ключа кэша поиска, но это потребовало бы повторяемого способа создавать идентичные строки из одинаковых хэшей, на что, как я слышал, нельзя полагаться (я думаю, мне придется построить строку самостоятельно после сортировки ключей).

У кого-нибудь есть мысли по этому поводу? Неправильно ли вызывать функцию с ключевыми аргументами и кэшировать результаты? Или просто очень сложно?

10
задан robru 6 June 2012 в 18:41
поделиться