6

次の関数は、すでに計算された値の結果を格納するデコレータとして使用することを目的としています。引数が以前にすでに計算されている場合、関数はcacheディクショナリに格納されている値を返します。

def cached(f):
    f.cache = {}
    def _cachedf(*args):
        if args not in f.cache:
            f.cache[args] = f(*args)

        return f.cache[args]

    return _cachedf

私は(誤って)それcacheが関数オブジェクトの属性である必要がないことに気づきました。実際のところ、次のコードも機能します。

def cached(f):
    cache = {}   # <---- not an attribute this time!
    def _cachedf(*args):
        if args not in cache:
            cache[args] = f(*args)

        return cache[args]
    return _cachedf

cacheオブジェクトが複数の呼び出しにわたって永続化する方法を理解するのに苦労しています。複数のキャッシュされた関数を数回呼び出してみましたが、競合や問題は見つかりませんでした。

関数が返さcacheれた後も変数がどのように存在するかを誰かが理解するのを手伝ってもらえますか?_cachedf

4

2 に答える 2

12

ここでクロージャを作成しています。関数は、囲んでいるスコープから_cachedf()変数を閉じます。cacheこれはcache、関数オブジェクトが存続する限り存続します。

編集:たぶん、これがPythonでどのように機能し、CPythonがこれをどのように実装するかについてもう少し詳しく追加する必要があります。

より簡単な例を見てみましょう。

def f():
    a = []
    def g():
        a.append(1)
        return len(a)
    return g

インタラクティブインタプリタでの使用例

>>> h = f()
>>> h()
1
>>> h()
2
>>> h()
3

関数を含むモジュールのコンパイル中f()に、コンパイラは、関数が囲んでいるスコープからg()名前を参照していることを確認し、この外部参照を関数に対応するコードオブジェクトに記憶します(具体的には、に名前を追加します)。af()af.__code__.co_cellvars

では、関数f()が呼び出されるとどうなりますか?最初の行は、新しいリストオブジェクトを作成し、それを名前にバインドしますa。次の行は、(モジュールのコンパイル中に作成されたコードオブジェクトを使用して)新しい関数オブジェクトを作成し、それを名前にバインドしますgg()この時点ではの本体は実行されず、最後に関数オブジェクトが返されます。

のコードオブジェクトにf()は、名前がローカル関数によって参照されているという注記があるaため、この名前の「セル」は、 f()が入力されたときに作成されます。aこのセルには、バインドされている実際のリストオブジェクトへの参照が含まれ、関数g()はこのセルへの参照を取得します。f()これにより、関数が終了してもリストオブジェクトとセルが有効に保たれます。

于 2012-08-06T14:52:02.687 に答える
3

_cachedf関数が返された後も、キャッシュ変数がどのように存在するかを理解するのを手伝ってもらえますか?

これは、Pythonの参照カウントガベージコレクターと関係があります。関数には変数への参照があり、呼び出し元には変数への参照があるcacheため、変数は保存されてアクセス可能になります。関数を再度呼び出すと、最初に作成されたものと同じ関数オブジェクトを使用しているため、キャッシュにアクセスできます。_cachedfcached

キャッシュへのすべての参照が破棄されるまで、キャッシュが失われることはありません。del演算子を使用してそれを行うことができます。

例えば:

>>> import time
>>> def cached(f):
...     cache = {}   # <---- not an attribute this time!
...     def _cachedf(*args):
...         if args not in cache:
...             cache[args] = f(*args)
...         return cache[args]
...     return _cachedf
...     
... 
>>> def foo(duration):
...     time.sleep(duration)
...     return True
...     
... 
>>> bob = cached(foo)
>>> bob(2) # Takes two seconds
True
>>> bob(2) # returns instantly
True
>>> del bob # Deletes reference to bob (aka _cachedf) which holds ref to cache
>>> bob = cached(foo)
>>> bob(2) # takes two seconds
True
>>> 

記録のために、あなたが達成しようとしているのはメモ化と呼ばれ同じことをしますが、デコレータクラスを使用する、より完全なメモ化デコレータがデコレータパターンページから入手できます。コードとクラスベースのデコレータは基本的に同じですが、クラスベースのデコレータは保存する前にハッシュ能力をチェックします。


編集(2017-02-02):@SiminJieのコメントは、cached(foo)(2)常に遅延が発生します。

これはcached(foo)、新しいキャッシュを持つ新しい関数を返すためです。がcached(foo)(2)呼び出されると、新しい新しい(空の)キャッシュが作成され、キャッシュされた関数がすぐに呼び出されます。

キャッシュは空であり、値が見つからないため、基になる関数を再実行します。代わりに、実行してから複数回cached_foo = cached(foo)呼び出します。cached_foo(2)これにより、最初の呼び出しの遅延のみが発生します。また、デコレータとして使用すると、期待どおりに機能します。

@cached
def my_long_function(arg1, arg2):
  return long_operation(arg1,arg2)

my_long_function(1,2) # incurs delay
my_long_function(1,2) # doesn't

デコレータに慣れていない場合は、この回答を見て、上記のコードの意味を理解してください。

于 2012-08-06T15:13:41.010 に答える