Как создать ограниченный декоратор памятки в Python? - PullRequest
8 голосов
/ 22 февраля 2012

Очевидно, что быстрый поиск дает миллион реализаций и разновидностей декоратора памятки в Python. Тем не менее, меня интересует аромат, который я не смог найти. Мне бы хотелось, чтобы он был таким, чтобы кэш хранимых значений мог иметь фиксированную емкость. Когда добавляются новые элементы, если достигается емкость, то самое старое значение удаляется и заменяется самым новым значением.

Меня беспокоит то, что, если я буду использовать памятку для хранения большого количества элементов, то программа будет аварийно завершать работу из-за нехватки памяти. (Я не знаю, насколько хорошо эта проблема может быть реализована на практике.) Если бы кэш был фиксированного размера, ошибка памяти не была бы проблемой. И многие проблемы, над которыми я работаю, меняются по мере выполнения программы, так что начальные кэшированные значения будут сильно отличаться от более поздних кэшированных значений (и с гораздо меньшей вероятностью будут повторяться позже). Вот почему я бы хотел, чтобы самые старые вещи были заменены новыми.

Я нашел класс OrderedDict и пример, показывающий, как создать его подкласс для указания максимального размера. Я хотел бы использовать это как мой кеш, а не обычный dict. Проблема в том, что мне нужно, чтобы декоратор memoize принял параметр с именем maxlen, который по умолчанию равен None. Если это None, то кеш безграничен и работает как обычно. Любое другое значение используется в качестве размера для кэша.

Я хочу, чтобы это работало так:

@memoize
def some_function(spam, eggs):
    # This would use the boundless cache.
    pass

и

@memoize(200)  # or @memoize(maxlen=200)
def some_function(spam, eggs):
    # This would use the bounded cache of size 200.
    pass

Ниже приведен код, который у меня есть, но я не вижу, как передать параметр в декоратор, заставляя его работать как «голым», так и с параметром.

import collections
import functools

class BoundedOrderedDict(collections.OrderedDict):
    def __init__(self, *args, **kwds):
        self.maxlen = kwds.pop("maxlen", None)
        collections.OrderedDict.__init__(self, *args, **kwds)
        self._checklen()

    def __setitem__(self, key, value):
        collections.OrderedDict.__setitem__(self, key, value)
        self._checklen()

    def _checklen(self):
        if self.maxlen is not None:
            while len(self) > self.maxlen:
                self.popitem(last=False)

def memoize(function):
    cache = BoundedOrderedDict()  # I want this to take maxlen as an argument
    @functools.wraps(function)
    def memo_target(*args):
        lookup_value = args
        if lookup_value not in cache:
            cache[lookup_value] = function(*args)
        return cache[lookup_value]
    return memo_target

@memoize
def fib(n):
    if n < 2: return 1
    return fib(n-1) + fib(n-2)

if __name__ == '__main__':
    x = fib(50)
    print(x)

Редактировать : Используя предложение Бена, я создал следующий декоратор, который, я считаю, работает так, как я себе представлял. Для меня важно иметь возможность использовать эти украшенные функции с multiprocessing, и это было проблемой в прошлом. Но быстрая проверка этого кода, казалось, работала правильно, даже если обрабатывать задания в пуле потоков.

def memoize(func=None, maxlen=None):
    if func:
        cache = BoundedOrderedDict(maxlen=maxlen)
        @functools.wraps(func)
        def memo_target(*args):
            lookup_value = args
            if lookup_value not in cache:
                cache[lookup_value] = func(*args)
            return cache[lookup_value]
        return memo_target
    else:
        def memoize_factory(func):
            return memoize(func, maxlen=maxlen)
        return memoize_factory

Ответы [ 3 ]

4 голосов
/ 22 февраля 2012
@memoize
def some_function(spam, eggs):
    # This would use the boundless cache.
    pass

Здесь memoize используется как функция, которая вызывается для одного аргумента функции и возвращает функцию.memoize является декоратором.

@memoize(200)  # or @memoize(maxlen=200)
def some_function(spam, eggs):
    # This would use the bounded cache of size 200.
    pass

Здесь memoize используется как функция, которая вызывается для одного целочисленного аргумента и возвращает функцию, а эта возвращенная функция сама используется в качестве декоратора, т.е.вызывается для одного аргумента функции и возвращает функцию.memoize - это фабрика декораторов .

Итак, чтобы объединить эти два, вам придется написать некрасивый код.Вероятно, я бы сделал так, чтобы memoize выглядел так:

def memoize(func=None, maxlen=None):
    if func:
        # act as decorator
    else:
        # act as decorator factory

Таким образом, если вы хотите передать параметры, вы всегда передаете их как аргументы ключевых слов, оставляя func (который должен быть позиционным параметром) не установлен, и если вы просто хотите, чтобы все было по умолчанию, он волшебным образом будет работать как декоратор напрямую.Это означает, что @memoize(200) выдаст вам ошибку;Вы могли бы избежать этого, выполнив некоторую проверку типов, чтобы увидеть, может ли вызываться func, что должно хорошо работать на практике, но на самом деле не очень «питонно».

Альтернативой может быть наличие двух разных декораторов.скажем memoize и bounded_memoize.Неограниченный memoize может иметь тривиальную реализацию, просто вызывая bounded_memoize с maxlen, установленным на None, так что это не будет стоить вам ничего при внедрении или обслуживании.

Обычно, как правило,thumb Я стараюсь избегать искажения функции для реализации двух только тангенциально связанных наборов функций, , особенно , когда они имеют такие разные подписи.Но в этом случае это делает использование декоратора естественным (требование @memoize() было бы весьма подвержено ошибкам, даже если это более согласованно с теоретической точки зрения), и вы, вероятно, собираетесь реализоватьодин раз и используйте его много раз, поэтому читаемость в точке использования, вероятно, является более важной задачей.

0 голосов
/ 22 февраля 2012

Вы хотите написать декоратор, который принимает аргумент (максимальная длина BoundedOrderedDict) и возвращает декоратор, который запомнит вашу функцию с BoundedOrderedDict соответствующего размера:

def boundedMemoize(maxCacheLen):
    def memoize(function):
        cache = BoundedOrderedDict(maxlen = maxCacheLen)
        def memo_target(*args):
            lookup_value = args
            if lookup_value not in cache:
                cache[lookup_value] = function(*args)
            return cache[lookup_value]
        return memo_target
    return memoize

Вы можете использовать его так:

@boundedMemoize(100)
def fib(n):
    if n < 2: return 1
    return fib(n - 1) + fib(n - 2)

Редактировать: Упс, пропущена часть вопроса. Если вы хотите, чтобы аргумент maxlen для декоратора был необязательным, вы можете сделать что-то вроде этого:

def boundedMemoize(arg):
    if callable(arg):
        cache = BoundedOrderedDict()
        @functools.wraps(arg)
        def memo_target(*args):
            lookup_value = args
            if lookup_value not in cache:
                cache[lookup_value] = arg(*args)
            return cache[lookup_value]
        return memo_target

    if isinstance(arg, int):
        def memoize(function):
            cache = BoundedOrderedDict(maxlen = arg)
            @functools.wraps(function)
            def memo_target(*args):
                lookup_value = args
                if lookup_value not in cache:
                    cache[lookup_value] = function(*args)
                return cache[lookup_value]
            return memo_target
        return memoize
0 голосов
/ 22 февраля 2012

С http://www.python.org/dev/peps/pep-0318/

Текущий синтаксис также позволяет объявлениям декоратора вызывать функцию, которая возвращает декоратор:

@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
    pass

Это эквивалентно:

func = decomaker(argA, argB, ...)(func)

Кроме того, я не уверен, что если бы я использовал OrderedDict для этого, я бы использовал кольцевой буфер, они очень просты в реализации.

...