Python 메모 / 지연 조회 속성 데코레이터


109

최근에 인스턴스 속성이 데이터베이스에 저장된 값을 반영하는 많은 클래스를 포함하는 기존 코드베이스를 살펴 보았습니다. 데이터베이스 조회가 지연되도록 이러한 속성을 많이 리팩토링했습니다. 생성자에서 초기화되지 않고 처음 읽을 때만 초기화됩니다. 이러한 속성은 인스턴스의 수명 동안 변경되지 않지만 처음 계산하는 데에는 실제 병목 현상이 발생하고 특수한 경우에만 실제로 액세스됩니다. 따라서 데이터베이스에서 검색된 후에도 캐시 될 수 있습니다 (따라서 입력이 단순히 "입력 없음" 인 메모 화 정의에 적합 ).

다양한 클래스의 다양한 속성에 대해 다음 코드 스 니펫을 반복해서 입력합니다.

class testA(object):

  def __init__(self):
    self._a = None
    self._b = None

  @property
  def a(self):
    if self._a is None:
      # Calculate the attribute now
      self._a = 7
    return self._a

  @property
  def b(self):
    #etc

내가 알지 못하는 파이썬에서 이미 이것을 수행하는 기존 데코레이터가 있습니까? 아니면 이것을 수행하는 데코레이터를 정의하는 합리적으로 간단한 방법이 있습니까?

저는 Python 2.5에서 작업하고 있지만 2.6 답변이 크게 다르면 여전히 흥미로울 수 있습니다.

노트

이 질문은 파이썬이이를 위해 많은 기성 데코레이터를 포함하기 전에 질문되었습니다. 용어를 수정하기 위해서만 업데이트했습니다.


저는 Python 2.7을 사용하고 있으며이를 위해 기성품 데코레이터에 대해 아무것도 볼 수 없습니다. 질문에 언급 된 기성 데코레이터에 대한 링크를 제공 할 수 있습니까?
Bamcclur

@Bamcclur 죄송합니다. 다른 댓글이 있는데 왜 삭제되었는지 잘 모르겠습니다. 지금 내가 찾을 수있는 유일한 것은 Python 3 functools.lru_cache()입니다.
detly

내장 (적어도 Python 2.7)이 있는지 확실하지 않지만 Boltons 라이브러리의 cachedproperty가 있습니다
guyarad

@guyarad 지금까지이 댓글을 보지 못했습니다. 그것은 환상적인 도서관입니다! 내가 그것을 찬성 할 수 있도록 답변으로 게시하십시오.
detly

답변:


12

모든 종류의 훌륭한 유틸리티를 위해 볼튼을 사용 하고 있습니다. 있습니다.

해당 라이브러리의 일부로 cachedproperty가 있습니다 .

from boltons.cacheutils import cachedproperty

class Foo(object):
    def __init__(self):
        self.value = 4

    @cachedproperty
    def cached_prop(self):
        self.value += 1
        return self.value


f = Foo()
print(f.value)  # initial value
print(f.cached_prop)  # cached property is calculated
f.value = 1
print(f.cached_prop)  # same value for the cached property - it isn't calculated again
print(f.value)  # the backing value is different (it's essentially unrelated value)

124

다음은 lazy 속성 데코레이터의 구현 예입니다.

import functools

def lazyprop(fn):
    attr_name = '_lazy_' + fn.__name__

    @property
    @functools.wraps(fn)
    def _lazyprop(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fn(self))
        return getattr(self, attr_name)

    return _lazyprop


class Test(object):

    @lazyprop
    def a(self):
        print 'generating "a"'
        return range(5)

대화식 세션 :

>>> t = Test()
>>> t.__dict__
{}
>>> t.a
generating "a"
[0, 1, 2, 3, 4]
>>> t.__dict__
{'_lazy_a': [0, 1, 2, 3, 4]}
>>> t.a
[0, 1, 2, 3, 4]

1
누군가 내부 기능에 적절한 이름을 추천 할 수 있습니까? 나는 ... 아침에 일을 이름에 그렇게 나쁘지 해요
마이크 보어

2
나는 일반적으로 내부 함수의 이름을 앞에 밑줄이있는 외부 함수와 동일하게 지정합니다. 그래서 "_lazyprop는"- 철학 격려 8의 "내부에서만 사용"다음
spenthil

1
이것은 훌륭하게 작동합니다 :) 왜 그런 중첩 함수에서 데코레이터를 사용하지 않았는지 모르겠습니다.
detly

4
비 데이터 설명자 프로토콜을 고려할 때이 프로토콜은 아래의 답변보다 훨씬 느리고 덜 우아합니다.__get__
Ronny

1
팁 : 문서 문자열 등을 @wraps(fn)@propertywrapsfunctools
풀지 않으

111

나는 이것을 직접 썼다 ... 진정한 일회성 계산 게으른 속성에 사용하기 위해 . 객체에 추가 속성을 붙이는 것을 피하고 활성화되면 속성 존재 여부 등을 확인하는 데 시간을 낭비하지 않기 때문에 좋아합니다.

import functools

class lazy_property(object):
    '''
    meant to be used for lazy evaluation of an object attribute.
    property should represent non-mutable data, as it replaces itself.
    '''

    def __init__(self, fget):
        self.fget = fget

        # copy the getter function's docstring and other attributes
        functools.update_wrapper(self, fget)

    def __get__(self, obj, cls):
        if obj is None:
            return self

        value = self.fget(obj)
        setattr(obj, self.fget.__name__, value)
        return value


class Test(object):

    @lazy_property
    def results(self):
        calcs = 1  # Do a lot of calculation here
        return calcs

참고 : lazy_property클래스는 비 데이터 설명자 이므로 읽기 전용입니다. __set__메서드를 추가하면 제대로 작동하지 않습니다.


9
이것은 이해하는 데 시간이 조금 걸렸지 만 절대적으로 놀라운 대답입니다. 함수 자체가 계산 된 값으로 대체되는 방식이 마음에 듭니다.
Paul Etherton 2013 년

2
후손을 위해 : 이것의 다른 버전은 이후 다른 답변에서 제안되었습니다 (참조 12 ). 이것은 Python 웹 프레임 워크에서 인기있는 것 같습니다 (파 생체는 Pyramid 및 Werkzeug에 있음).
André Caron

1
Werkzeug에 werkzeug.utils.cached_property : werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira

3
이 방법은 선택한 답변보다 7.6 배 더 빠릅니다. (2.45 µs / 322 ns) ipython 노트북 참조
Dave Butler

1
주의 : 이것은 방법 에 대한 할당fget방해 @property하지 않습니다. 불변성 / 멱 등성을 보장하려면 발생하는 __set__()메서드 를 추가해야합니다 AttributeError('can\'t set attribute')(또는 예외 / 메시지가 적합한 것이지만 이것이 발생하는 것입니다 property). 불행히도 이는 두 번째 및 후속 액세스 __get__()에서 dict 에서 fget 값을 가져 오는 대신 각 액세스에서 호출 되기 때문에 1 마이크로 초의 성능 영향을 받습니다. 내 유스 케이스의 핵심 인 불변성 / 멱 등성을 유지하는 것이 내 의견으로는 가치가 있지만 YMMV.
scanny

4

여기에, 옵션으로 타임 아웃 인수를하는 호출을의 __call__당신은 또한 이상 복사 할 수 __name__, __doc__, __module__FUNC의 네임 스페이스 :

import time

class Lazyproperty(object):

    def __init__(self, timeout=None):
        self.timeout = timeout
        self._cache = {}

    def __call__(self, func):
        self.func = func
        return self

    def __get__(self, obj, objcls):
        if obj not in self._cache or \
          (self.timeout and time.time() - self._cache[key][1] > self.timeout):
            self._cache[obj] = (self.func(obj), time.time())
        return self._cache[obj]

전의:

class Foo(object):

    @Lazyproperty(10)
    def bar(self):
        print('calculating')
        return 'bar'

>>> x = Foo()
>>> print(x.bar)
calculating
bar
>>> print(x.bar)
bar
...(waiting 10 seconds)...
>>> print(x.bar)
calculating
bar

3

property클래스입니다. 설명은 정확합니다. 단순히 그것에서 파생되고 원하는 동작을 구현하십시오.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')

3

당신이 정말 원하는 것은입니다 reify(! 소스 연결) 피라미드에서 장식 :

클래스 메서드 데코레이터로 사용합니다. 파이썬 @property데코레이터 와 거의 똑같이 작동 하지만, 데코레이션 한 메소드의 결과를 첫 번째 호출 후 인스턴스 dict에 넣어서 데코레이션하는 함수를 인스턴스 변수로 효과적으로 대체합니다. 파이썬 용어로 비 데이터 설명자입니다. 다음은 예제와 사용법입니다.

>>> from pyramid.decorator import reify

>>> class Foo(object):
...     @reify
...     def jammy(self):
...         print('jammy called')
...         return 1

>>> f = Foo()
>>> v = f.jammy
jammy called
>>> print(v)
1
>>> f.jammy
1
>>> # jammy func not called the second time; it replaced itself with 1
>>> # Note: reassignment is possible
>>> f.jammy = 2
>>> f.jammy
2

1
좋은 것, 내가 필요한 것을 정확히 수행합니다 ... 비록 피라미드가 한 데코레이터에게 큰 의존성이긴하지만:)
detly

@detly 데코레이터 구현은 간단하며 pyramid종속성이 필요하지 않고 직접 구현할 수 있습니다.
Peter Wood

따라서 링크는 "소스 연결"말한다 : D
안티 Haapala

@AnttiHaapala 나는 눈치 채었지만 링크를 따르지 않는 사람들을 위해 구현하는 것이 간단하다는 것을 강조하고 싶다고 생각했습니다.
Peter Wood

1

지금까지 문제와 답변 모두에서 용어의 혼합 및 / 또는 개념의 혼란이 있습니다.

지연 평가는 값이 필요한 마지막 가능한 순간에 런타임에 무언가가 평가된다는 것을 의미합니다. 표준@property 데코레이터가 그렇게합니다.(*) 데코 레이팅 된 함수는 해당 속성의 값이 필요할 때마다 평가됩니다. (게으른 평가에 대한 위키피디아 기사 참조)

(*) 실제로 진정한 게으른 평가 (예 : 하스켈 비교)는 파이썬에서 달성하기가 매우 어렵습니다 (그리고 관용적이지 않은 코드가 생성됩니다).

메모 화는 질문자가 찾고있는 것처럼 보이는 정확한 용어입니다. 반환 값 평가에 대한 부작용에 의존하지 않는 순수 함수는 안전하게 메모 할 수 있으며 실제로 functools에 데코레이터가 @functools.lru_cache있으므로 특수한 동작이 필요하지 않는 한 자체 데코레이터를 작성할 필요가 없습니다.


원래 구현에서는 멤버가 개체 초기화시 DB에서 계산 / 검색 되었기 때문에 "lazy"라는 용어를 사용했으며 속성이 실제로 템플릿에서 사용될 때까지 해당 계산을 연기하고 싶습니다. 이것은 게으름의 정의와 일치하는 것 같았습니다. 나는 내 질문이 이미를 사용하는 솔루션을 가정하고 있기 때문에 @property"lazy"는 그 시점에서 많은 의미가 없다는 것에 동의합니다 . (저는 또한 메모 화를 캐시 된 출력에 대한 입력 맵으로 생각했습니다. 이러한 속성에는 입력이 하나뿐이기 때문에 맵이 필요 이상으로 복잡해 보였습니다.)
detly

사람들이 "즉시 사용 가능한"솔루션으로 제안한 모든 데코레이터는 내가 이것을 물었을 때도 존재하지 않았습니다.
detly

나는 Jason과 동의합니다. 이것은 게으른 평가가 아닌 캐싱 / 메모리에 관한 질문입니다.
poindexter

@poindexter - 캐싱하지 않습니다 매우 그것을 커버; 객체 초기화 시간에 값을 찾고 캐싱하는 것과 속성에 액세스 할 때 값을 찾고 캐싱하는 것과 구별하지 않습니다 (여기서 핵심 기능). 나는 그것을 무엇이라고 부를까? "처음 사용 후 캐시"데코레이터?
detly

@detly Memoize. Memoize라고 불러야합니다. en.wikipedia.org/wiki/Memoization
포인 덱스터

0

Python 기본 속성에서 클래스를 빌드하면이 작업을 쉽고 간단하게 수행 할 수 있습니다.

class cached_property(property):
    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__, None)
        if value is None:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value

이 속성 클래스를 일반 클래스 속성처럼 사용할 수 있습니다 (보시다시피 항목 할당도 지원합니다).

class SampleClass():
    @cached_property
    def cached_property(self):
        print('I am calculating value')
        return 'My calculated value'


c = SampleClass()
print(c.cached_property)
print(c.cached_property)
c.cached_property = 2
print(c.cached_property)
print(c.cached_property)

값은 처음 계산 한 후 저장 한 값을 사용했습니다.

산출:

I am calculating value
My calculated value
My calculated value
2
2
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.