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でこれを行うための既存のデコレータはありますか?または、これを行うデコレータを定義するための合理的に簡単な方法はありますか?

私はPython 2.5で作業していますが、2.6の回答が大幅に異なる場合は、それでも興味深いかもしれません。

注意

この質問は、Pythonがこのための既製のデコレータを多数含む前に行われました。用語を修正するためだけに更新しました。


私はPython 2.7を使用していますが、このための既製のデコレーターについては何も見ていません。質問で言及されている既製のデコレータへのリンクを提供できますか?
Bamcclur

@Bamcclur申し訳ありませんが、以前はそれらを詳述する他のコメントがありましたが、なぜ削除されたのかはわかりません。私が今見つけることができる唯一のものは、Python 3のものです:functools.lru_cache()
16

確認がされていない組み込み関数(少なくとも、Pythonの2.7は)、しかしボルトンズ図書館のありますcachedproperty
guyarad

@guyarad私は今までこのコメントを見ませんでした。それは素晴らしいライブラリです!それを回答として投稿して、賛成投票できるようにしてください。
2017年

回答:


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

次に、遅延プロパティデコレータの実装例を示します。

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
誰かが内部関数に適切な名前を推奨できますか?朝の名前付けが苦手です...
Mike Boers

2
私は通常、内部関数に外部関数と同じ名前を付け、前にアンダースコアを付けます。「_lazyprop」だから- PEP 8の哲学「社外秘」に続く
spenthil

1
これはうまく機能します:)そのようにネストされた関数でデコレータを使用することがなぜ起こらなかったのか、私にはわかりません。
間違いなく2010年

4
非データ記述子プロトコルを考えると、これは、以下の回答を使用した場合よりも遅く、エレガントではありません__get__
Ronny

1
ヒント:ドキュメント文字列などを@wraps(fn)@propertywrapsfunctools
失わ

111

私はこれを自分で書きました...真の1回限りの計算された遅延プロパティに使用されます。オブジェクトに余分な属性を貼り付けることを回避し、一度アクティブにすると属性の存在などをチェックする時間を無駄にしないので、私はそれが好きです。

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
後世のために:これの他のバージョンは、以来、他の回答で提案されています(参照1および2)。これはPythonウェブフレームワークで人気があるようです(派生物はPyramidとWerkzeugに存在します)。
アンドレ・キャノン

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ノートブックを参照してください
デイブバトラー

1
注意:これfgetウェイへの割り当て妨げません@property。不変性/べき等性を確保するには、発生する__set__()メソッドAttributeError('can\'t set attribute')(または、例外/メッセージに適したものであれば、これが発生するものproperty)を追加する必要があります。残念ながら、2番目以降のアクセスでdict__get__()からfget値をプルするのではなく、アクセスごとに呼び出されるため、マイクロ秒の数分の1のパフォーマンスへの影響が発生します。不変/べき等性を維持することは私の意見では十分価値があります。これは、私のユースケースにとって重要ですが、YMMVです。
スキャンニー

4

ここでは、オプションのtimeout引数をとる呼び出し可能だ__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

あなたが本当に望んでいるのはPyramidのreify(source linked!)デコレーターです

クラスメソッドデコレータとして使用します。これはPython @propertyデコレータとほぼ同じように動作しますが、最初の呼び出しの後に、デコレートするメソッドの結果をインスタンスdictに入れ、デコレートする関数をインスタンス変数で効果的に置き換えます。Pythonの用語では、これは非データ記述子です。以下は、例とその使用方法です。

>>> 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
素晴らしいもの、私が必要とするものを正確に実行します... Pyramidは1つのデコレータの大きな依存関係である可能性があります:)
間違いなく

@detlyデコレータの実装は簡単で、pyramid依存関係を必要とせずに自分で実装できます。
ピーターウッド

したがって、リンクは、「ソースが連結された」と述べている:D
アンティHaapala

@AnttiHaapala私は気づきましたが、リンクをたどらない人のために実装するのは簡単であることを強調したいと思いました。
ピーターウッド

1

これまでのところ、問題と回答の両方で、用語の混同や概念の混乱があります。

遅延評価は、値が必要とされる可能な最後の瞬間に実行時に何かが評価されることを意味します。標準の@propertyデコレータはまさにそれを行います。(*)デコレートされた関数は、そのプロパティの値が必要になるたびにのみ評価されます。(遅延評価に関するウィキペディアの記事を参照)

(*)実際には、Pythonで真の遅延評価(たとえば、haskellと比較)を達成するのは非常に困難です(そして、コードが慣用的とはほど遠い結果になります)。

メモ化は、質問者が探しているように見えるものの正しい用語です。戻り値の評価の副作用に依存しない純粋な関数は安全にメモ化でき、functools には実際にデコレーターがある@functools.lru_cacheため、特別な動作が必要でない限り、独自のデコレーターを作成する必要はありません。


元の実装では、メンバーはオブジェクトの初期化時にDBから計算/取得され、プロパティが実際にテンプレートで使用されるまでその計算を延期するため、「遅延」という用語を使用しました。これは怠惰の定義と一致しているように思えました。私の質問はすでにを使用した解決策を前提としている@propertyため、その時点では「遅延」はあまり意味がありません。(メモ化は、キャッシュされた出力への入力のマップとしても考えられました。これらのプロパティには入力が1つしかないため、マップは必要以上に複雑に見えました。)
間違いなく

私がこれを尋ねたときも、人々が「すぐに使える」ソリューションとして提案したすべてのデコレータは存在しなかったことに注意してください。
2016

私はジェイソンに同意します、これは遅延評価ではなくキャッシング/メモ化に関する質問です。
poindexter 2016年

@poindexter-キャッシングはそれを完全にカバーしません。オブジェクトの初期化時に値を検索してキャッシュすることと、プロパティがアクセスされたときに値を検索してキャッシュすることを区別しません(ここで重要な機能です)。何といいますか?「使用後のキャッシュ」デコレーター?
16年

@detly Memoize。あなたはそれをメモ化と呼ぶべきです。 en.wikipedia.org/wiki/
メモ化

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.