단순히 함수 반환 값을 캐시하는 데코레이터가 있습니까?


165

다음을 고려하세요:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

나는 새롭지 만 캐싱을 데코레이터로 고려할 수 있다고 생각합니다. 나는 그것과 같은 것을 찾지 못했습니다.)

PS 실제 계산은 변경 가능한 값에 의존하지 않습니다.


그런 기능을 가진 데코레이터가있을 수 있지만 원하는 것을 완전히 지정하지 않았습니다. 어떤 종류의 캐싱 백엔드를 사용하고 있습니까? 그리고 가치는 어떻게 입력 될까요? 나는 당신이 정말로 요구하는 것이 캐시 된 읽기 전용 속성이라고 코드에서 가정하고 있습니다.
David Berger

"캐싱"이라고 부르는 것을 수행하는 메모 데코레이터가 있습니다. 그들은 일반적으로 (메소드가 되든 아니든) 그 결과가 인수에 의존하는 (self!-와 같은 변경 가능한 것들에 의존하지 않음) 함수에서 작동하므로 별도의 메모 사전을 유지합니다.
Alex Martelli

답변:


211

Python 3.2부터 내장 데코레이터가 있습니다.

@functools.lru_cache(maxsize=100, typed=False)

가장 최근 호출을 최대 크기까지 저장하는 메모가있는 콜러 블로 함수를 래핑하는 데코레이터입니다. 비용이 많이 들거나 I / O 바운드 함수가 동일한 인수로 주기적으로 호출 될 때 시간을 절약 할 수 있습니다.

피보나치 수 계산을위한 LRU 캐시의 예 :

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> print([fib(n) for n in range(16)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> print(fib.cache_info())
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

Python 2.x를 사용하는 경우 호환 가능한 다른 메모 라이브러리 목록은 다음과 같습니다.



백 포트는 이제 여기에서 찾을 수 있습니다 : pypi.python.org/pypi/backports.functools_lru_cache
Frederick Nord

이론상 @gerrit는 일반적으로 해시 가능한 객체에 대해 작동합니다. 일부 해시 가능한 객체는 동일한 객체 인 경우에만 동일합니다 (예 : 명시 적 __hash __ () 함수가없는 사용자 정의 객체).
Jonathan

1
@Jonathan 작동하지만 잘못되었습니다. 해시 가능하고 변경 가능한 인수를 전달하고 함수의 첫 번째 호출 이후에 객체의 값을 변경하면 두 번째 호출은 원본이 아닌 변경된 객체를 반환합니다. 그것은 거의 확실히 사용자가 원하는 것이 아닙니다. 변경 가능한 인수에 대해 작동하려면 lru_cache캐싱 결과의 복사본을 만들어야하며 functools.lru_cache구현 에서 그러한 복사본이 만들어지지 않습니다 . 그렇게하면 큰 개체를 캐시하는 데 사용할 때 찾기 어려운 메모리 문제가 발생할 위험이 있습니다.
gerrit

@gerrit 여기를 팔로우 해 주시겠습니까 ? stackoverflow.com/questions/44583381/… ? 나는 당신의 모범을 완전히 따르지 않았습니다.
Jonathan

30

범용 메모 화 데코레이터를 요구 하지 않는 것처럼 들립니다 (즉, 다른 인수 값에 대한 반환 값을 캐시하려는 일반적인 경우에는 관심이 없습니다). 즉, 다음이 필요합니다.

x = obj.name  # expensive
y = obj.name  # cheap

범용 메모 데코레이터는 다음을 제공합니다.

x = obj.name()  # expensive
y = obj.name()  # cheap

메서드 호출 구문이 더 나은 스타일이라고 제출합니다. 왜냐하면 속성 구문은 빠른 조회를 제안하는 동안 값 비싼 계산 가능성을 제안하기 때문입니다.

[업데이트 : 이전에 여기에 연결하고 인용 한 클래스 기반 메모 데코레이터는 메서드에서 작동하지 않습니다. 데코레이터 기능으로 교체했습니다.] 범용 메모 데코레이터를 사용하려는 경우 다음과 같은 간단한 방법이 있습니다.

def memoize(function):
  memo = {}
  def wrapper(*args):
    if args in memo:
      return memo[args]
    else:
      rv = function(*args)
      memo[args] = rv
      return rv
  return wrapper

사용 예 :

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

캐시 크기에 제한이있는 또 다른 메모 데코레이터는 여기 에서 찾을 수 있습니다 .


모든 답변에 언급 된 데코레이터 중 어느 것도 메서드에서 작동하지 않습니다! 아마도 그들이 수업 기반이기 때문일 것입니다. 오직 하나의 자아 만 전달됩니까? 다른 것들은 잘 작동하지만 함수에 값을 저장하는 것은 어렵습니다.
Tobias

2
args가 해시 가능하지 않으면 문제가 발생할 수 있다고 생각합니다.
알 수 없음

1
@Unknown 예, 여기에서 인용 한 첫 번째 데코레이터는 해시 가능한 유형으로 제한됩니다. ActiveState (캐시 크기 제한 있음)에있는 것은 인수를 (해시 가능한) 문자열로 피클합니다. 이는 물론 더 비싸지 만 더 일반적입니다.
Nathan Kitchen

@vanity 클래스 기반 데코레이터의 한계를 지적 해 주셔서 감사합니다. 메서드에서 작동하는 데코레이터 기능을 보여주기 위해 내 대답을 수정했습니다 (실제로 이것을 테스트했습니다).
Nathan Kitchen

1
@SiminJie 데코레이터는 한 번만 호출되며 반환되는 래핑 된 함수는 fibonacci. 이 함수는 항상 동일한 memo사전을 사용합니다 .
Nathan Kitchen

22
class memorize(dict):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        result = self[key] = self.func(*key)
        return result

샘플 용도 :

>>> @memorize
... def foo(a, b):
...     return a * b
>>> foo(2, 4)
8
>>> foo
{(2, 4): 8}
>>> foo('hi', 3)
'hihihi'
>>> foo
{(2, 4): 8, ('hi', 3): 'hihihi'}

이상한! 어떻게 작동합니까? 내가 본 다른 데코레이터처럼 보이지 않습니다.
PascalVKooten

1
이 솔루션은 키워드 인수를 사용하는 경우 TypeError를 반환합니다 (예 : foo (3, b = 5))
kadee

1
솔루션의 문제는 메모리 제한이 없다는 것입니다. 명명 된 인수에 관해서는, 당신은 추가 할 수 있습니다에 __ call__ 및 __ missing__ ** nargs 같은
레오 니드 Mednikov

20

Python 3.8 functools.cached_property데코레이터

https://docs.python.org/dev/library/functools.html#functools.cached_property

cached_propertyWerkzeug의 내용은 https://stackoverflow.com/a/5295190/895245 에서 언급 되었지만 파생 된 버전이 3.8로 병합 될 것입니다.

이 데코레이터는 캐싱 @property또는 @functools.lru_cache인수가없는 경우 클리너로 볼 수 있습니다 .

문서는 다음과 같이 말합니다.

@functools.cached_property(func)

클래스의 메서드를 값이 한 번 계산 된 다음 인스턴스 수명 동안 일반 속성으로 캐시되는 속성으로 변환합니다. 캐싱이 추가 된 property ()와 유사합니다. 그렇지 않으면 사실상 변경 불가능한 인스턴스의 값 비싼 계산 된 속성에 유용합니다.

예:

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

버전 3.8의 새로운 기능.

참고이 데코레이터는 각 인스턴스 의 dict 속성이 변경 가능한 매핑이어야합니다. 즉, 메타 클래스 ( 유형 인스턴스 의 dict 속성은 클래스 네임 스페이스에 대한 읽기 전용 프록시 이기 때문에)와 같은 일부 유형 과 정의 된 슬롯 중 하나로 dict 를 포함하지 않고 슬롯 을 지정하는 유형 (클래스와 같은)에서는 작동하지 않습니다. dict 속성을 전혀 제공하지 마십시오 ).



9

이 간단한 데코레이터 클래스를 코딩하여 함수 응답을 캐시했습니다. 내 프로젝트에 매우 유용합니다.

from datetime import datetime, timedelta 

class cached(object):
    def __init__(self, *args, **kwargs):
        self.cached_function_responses = {}
        self.default_max_age = kwargs.get("default_cache_max_age", timedelta(seconds=0))

    def __call__(self, func):
        def inner(*args, **kwargs):
            max_age = kwargs.get('max_age', self.default_max_age)
            if not max_age or func not in self.cached_function_responses or (datetime.now() - self.cached_function_responses[func]['fetch_time'] > max_age):
                if 'max_age' in kwargs: del kwargs['max_age']
                res = func(*args, **kwargs)
                self.cached_function_responses[func] = {'data': res, 'fetch_time': datetime.now()}
            return self.cached_function_responses[func]['data']
        return inner

사용법은 간단합니다.

import time

@cached
def myfunc(a):
    print "in func"
    return (a, datetime.now())

@cached(default_max_age = timedelta(seconds=6))
def cacheable_test(a):
    print "in cacheable test: "
    return (a, datetime.now())


print cacheable_test(1,max_age=timedelta(seconds=5))
print cacheable_test(2,max_age=timedelta(seconds=5))
time.sleep(7)
print cacheable_test(3,max_age=timedelta(seconds=5))

1
첫 번째 @cached에 괄호가 없습니다. 그렇지 않으면에만 반환 cached대신에 오브젝트를 myfunc하고 호출 할 때 myfunc()다음이 inner항상 반환 값으로 반환됩니다
마르쿠스 Meskanen을

6

면책 조항 : 저는 kids.cache 의 저자입니다 .

을 확인해야 합니다. python 2 및 python 3에서 작동 kids.cache하는 @cache데코레이터를 제공합니다. 종속성 없음, ~ 100 줄의 코드. 예를 들어 코드를 염두에두고 사용하는 것은 매우 간단합니다. 다음과 같이 사용할 수 있습니다.

pip install kids.cache

그때

from kids.cache import cache
...
class MyClass(object):
    ...
    @cache            # <-- That's all you need to do
    @property
    def name(self):
        return 1 + 1  # supposedly expensive calculation

또는 (동일한 결과) @cache뒤에 데코레이터를 넣을 수 @property있습니다.

속성에 캐시를 사용하면이라고 게으른 평가 , kids.cache훨씬 더 (가 ... 인수, 속성, 메서드의 모든 종류, 심지어 클래스와 기능을 작동) 할 수 있습니다. 고급 사용자의 경우 python 2 및 python 3 (LRU, LFU, TTL, RR 캐시)에 멋진 캐시 저장소를 제공하는 kids.cache지원 cachetools.

중요 참고 :의 기본 캐시 저장소 kids.cache는 표준 dict이며, 이는 계속해서 증가하는 캐싱 저장소로 이어질 수 있으므로 쿼리가 다른 장기 실행 프로그램에는 권장되지 않습니다. 이 사용법을 위해 예를 들어 ( @cache(use=cachetools.LRUCache(maxsize=2))함수 / 속성 / 클래스 / 메소드를 장식하기 위해 ...)를 사용하여 다른 캐시 저장소를 플러그인 할 수 있습니다 .


이 모듈은 파이썬 2 ~ 0.9s에서 느린 가져 오기 시간을 초래하는 것 같습니다 (참조 : pastebin.com/raw/aA1ZBE9Z ). 나는 이것이 github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 라인 때문이라고 생각합니다 (setuptools 진입 점 참조). 나는 이것에 대한 문제를 만들고 있습니다.
Att Righ 2011

위의 github.com/0k/kids.cache/issues/9에 대한 문제가 있습니다.
Att Righ 2011

이로 인해 메모리 누수가 발생합니다.
Timothy Zhang

@vaab의 인스턴스 c를 만들고으로 MyClass검사 objgraph.show_backrefs([c], max_depth=10)하면 클래스 개체 MyClass에서 c. 즉, c출시 될 때까지 출시되지 않았을 것 MyClass입니다.
Timothy Zhang

@TimothyZhang 여러분을 초대하고 github.com/0k/kids.cache/issues/10에 우려 사항을 추가 할 수 있습니다 . Stackoverflow는 이에 대해 적절한 논의를하기에 적합한 장소가 아닙니다. 그리고 추가 설명이 필요합니다. 의견을 보내 주셔서 감사합니다.
vaab

5

아, 이것에 대한 올바른 이름을 찾기 위해 필요했습니다 : " 지연 속성 평가 ".

나도 많이합니다. 언젠가 내 코드에서 그 레시피를 사용할 것입니다.


4

fastcache 이다 ". 파이썬 3 functools.lru_cache의 C 구현 표준 라이브러리를 통해 10-30x의 속도 향상을 제공합니다."

선택한 답변 과 동일하며 가져 오기만 다릅니다.

from fastcache import lru_cache
@lru_cache(maxsize=128, typed=False)
def f(a, b):
    pass

또한 설치 해야하는 functools와 달리 Anaconda에 설치되어 제공 됩니다 .


1
functools표준 라이브러리의 일부입니다. 게시 한 링크는 임의의 git fork 또는 다른 것입니다.
cz


3

Django Framework를 사용하는 경우 API 사용에 대한 뷰 또는 응답을 캐시하는 속성이 @cache_page(time)있으며 다른 옵션도있을 수 있습니다.

예:

@cache_page(60 * 15, cache="special_cache")
def my_view(request):
    ...

자세한 내용은 여기에서 확인할 수 있습니다 .


2

Memoize 예제 와 함께 다음 파이썬 패키지를 찾았습니다.

  • cachepy ; 캐시 된 함수에 대한 ttl 및 \ 또는 호출 수를 설정할 수 있습니다. 또한 암호화 된 파일 기반 캐시를 사용할 수 있습니다.
  • percache

1

지속성을 위해 pickle을 사용하고 거의 확실하게 고유 한 짧은 ID에 sha1을 사용하여 이와 같은 것을 구현했습니다. 기본적으로 캐시는 함수 코드와 인수 히스 토를 해시하여 sha1을 얻은 다음 이름에 해당 sha1이있는 파일을 찾습니다. 존재한다면 그것을 열고 결과를 반환했습니다. 그렇지 않은 경우 함수를 호출하고 결과를 저장합니다 (선택적으로 처리하는 데 일정 시간이 걸린 경우에만 저장).

즉,이 작업을 수행 한 기존 모듈을 찾았고 여기에서 해당 모듈을 찾으려고합니다. 내가 찾을 수있는 가장 가까운 것은 이것이 바로 http : //chase-seibert.github입니다. io / blog / 2011 / 11 / 23 / pythondjango-disk-based-caching-decorator.html

내가 볼 수있는 유일한 문제는 거대한 배열에 고유하지 않은 str (arg)를 해시하기 때문에 큰 입력에 대해 잘 작동하지 않는다는 것입니다.

클래스가 내용의 보안 해시를 반환하는 unique_hash () 프로토콜 이 있으면 좋을 것 입니다. 나는 기본적으로 내가 신경 쓰는 유형에 대해 수동으로 구현했습니다.



1

Django를 사용 중이고 뷰를 캐시하려면 Nikhil Kumar의 답변을 참조하십시오 .


그러나 모든 함수 결과를 캐시하려면 django-cache-utils를 사용할 수 있습니다 .

Django 캐시를 재사용하고 사용하기 쉬운 cached데코레이터 를 제공합니다 .

from cache_utils.decorators import cached

@cached(60)
def foo(x, y=0):
    print 'foo is called'
    return x+y

1

@lru_cache 기본 기능 값으로는 완벽하지 않습니다.

mem데코레이터 :

import inspect


def get_default_args(f):
    signature = inspect.signature(f)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }


def full_kwargs(f, kwargs):
    res = dict(get_default_args(f))
    res.update(kwargs)
    return res


def mem(func):
    cache = dict()

    def wrapper(*args, **kwargs):
        kwargs = full_kwargs(func, kwargs)
        key = list(args)
        key.extend(kwargs.values())
        key = hash(tuple(key))
        if key in cache:
            return cache[key]
        else:
            res = func(*args, **kwargs)
            cache[key] = res
            return res
    return wrapper

테스트 용 코드 :

from time import sleep


@mem
def count(a, *x, z=10):
    sleep(2)
    x = list(x)
    x.append(z)
    x.append(a)
    return sum(x)


def main():
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5, z=6))
    print(count(1,2,3,4,5, z=6))
    print(count(1))
    print(count(1, z=10))


if __name__ == '__main__':
    main()

결과-수면시 단 3 회

하지만 @lru_cache4 배가 될 것입니다.

print(count(1))
print(count(1, z=10))

두 번 계산됩니다 (기본값으로 작동하지 않음).


0

functools.cachePython 3.9에서 릴리스되었습니다 ( docs ) :

from functools import cache

@cache
def factorial(n):
    return n * factorial(n-1) if n else 1

이전 버전 에서 초기 답변 중 하나는lru_cache 제한 및 lru 기능이없는 일반 캐시로 사용하는 유효한 솔루션 입니다. ( 문서 )

maxsize가 None으로 설정되면 LRU 기능이 비활성화되고 캐시가 제한없이 커질 수 있습니다.

여기에 더 예쁜 버전이 있습니다.

cache = lru_cache(maxsize=None)

@cache
def func(param1):
   pass
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.