2012-06-06 2 views
6

最近私はメモ作成に関する質問をしていくつかの大きな回答を得ましたが、今度はそれを次のレベルに引き上げたいと思います。かなりのグーグルで、私はキーワード引数を取った関数をキャッシュすることができたmemoizeデコレータのリファレンス実装を見つけることができませんでした。実際には、ほとんどの人がキャッシュルックアップのキーとして単に*argsを使用しました。つまり、リストやdictsを引数として受け入れる関数をメモしたいと思っていた場合にも壊れます。Pythonでmemoizeデコレータのキーワード引数をサポートするためのpyononicな方法はありますか?

私の場合、関数の最初の引数は、キャッシュルックアップのdictキーとして使用するのに適した一意の識別子ですが、キーワード引数を使用して同じキャッシュにアクセスする必要がありました。つまり、my_func('unique_id', 10)my_func(foo=10, func_id='unique_id')の両方が同じキャッシュ結果を返すはずです。

これを実行するには、「最初の引数に対応するキーワードのどれかを調べる」というクリーンで非凡な方法が必要です。これは私が思いついたものです:

class memoize(object): 
    def __init__(self, cls): 
     if type(cls) is FunctionType: 
      # Let's just pretend that the function you gave us is a class. 
      cls.instances = {} 
      cls.__init__ = cls 
     self.cls = cls 
     self.__dict__.update(cls.__dict__) 

    def __call__(self, *args, **kwargs): 
     """Return a cached instance of the appropriate class if it exists.""" 
     # This is some dark magic we're using here, but it's how we discover 
     # that the first argument to Photograph.__init__ is 'filename', but the 
     # first argument to Camera.__init__ is 'camera_id' in a general way. 
     delta = 2 if type(self.cls) is FunctionType else 1 
     first_keyword_arg = [k 
      for k, v in inspect.getcallargs(
       self.cls.__init__, 
       'self', 
       'first argument', 
       *['subsequent args'] * (len(args) + len(kwargs) - delta)).items() 
        if v == 'first argument'][0] 
     key = kwargs.get(first_keyword_arg) or args[0] 
     print key 
     if key not in self.cls.instances: 
      self.cls.instances[key] = self.cls(*args, **kwargs) 
     return self.cls.instances[key] 

これは実際に動作することです。あなたはこのように飾る場合などは、:あなたのコードから次に

@memoize 
class FooBar: 
    instances = {} 

    def __init__(self, unique_id, irrelevant=None): 
     print id(self) 

あなたが実際にFooBarのの同じインスタンスを取得FooBar('12345', 20)またはFooBar(irrelevant=20, unique_id='12345')のいずれかを呼び出すことができます。最初の引数とは異なる名前の別のクラスを定義することができます。一般的な方法で動作します(つまり、デコレータは、これを動作させるために装飾するクラスについて何も知る必要はありません)。

問題がある

は、それは不信心混乱;-) inspect.getcallargsは、あなたがそれを指定する引数に定義されたキーワードをマッピングする辞書を返すので、それが動作

ですので、私はいくつかの偽の引数を供給し、その後のための辞書を調べ渡された最初の引数。

このようなことが存在するのであれば、両方の種類の引数をキーワード引数の代わりに引数のリストとして統一したinspect.getcallargsと類似しています。つまり、このようなことが可能になる:

def __call__(self, *args, **kwargs): 
    key = inspect.getcallargsaslist(self.cls.__init__, None, *args, **kwargs)[1] 
    if key not in self.cls.instances: 
     self.cls.instances[key] = self.cls(*args, **kwargs) 
    return self.cls.instances[key] 

私が直接参照キャッシュキーとしてinspect.getcallargsが提供する辞書を使用することになり、これを取り組んで見ることができ、他の方法、それから、同一の文字列を作るために繰り返し可能な方法を必要とします私が聞いたことがあるものであるとは限りません(私はキーをソートした後に自分自身で文字列を構築しなければならないと思います)。

誰もがこれに関する考えを持っていますか?キーワード引数を持つ関数を呼び出して結果をキャッシュするのは間違っていますか?それとも非常に難しい?

答えて

4

にバックポートすることができます。

import inspect 

class key_memoized(object): 
    def __init__(self, func): 
     self.func = func 
     self.cache = {} 

    def __call__(self, *args, **kwargs): 
     key = self.key(args, kwargs) 
     if key not in self.cache: 
      self.cache[key] = self.func(*args, **kwargs) 
     return self.cache[key] 

    def normalize_args(self, args, kwargs): 
     spec = inspect.getargs(self.func.__code__).args 
     return dict(kwargs.items() + zip(spec, args)) 

    def key(self, args, kwargs): 
     a = self.normalize_args(args, kwargs) 
     return tuple(sorted(a.items())) 

例:

@key_memoized 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar + baz + spam 

print foo(1, 2, 3) 
print foo(1, 2, spam=3)   #memoized 
print foo(spam=3, baz=2, bar=1) #memoized 

注意をさらにkey_memoizedを拡張し、より具体的なメモ化戦略を提供するためにkey()メソッドをオーバーライドすることもできます。引数の一部を無視する:

興味深い
class memoize_by_bar(key_memoized): 
    def key(self, args, kwargs): 
     return self.normalize_args(args, kwargs)['bar'] 

@memoize_by_bar 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar 

print foo('x', 'ignore1', 'ignore2') 
print foo('x', 'ignore3', 'ignore4') 
+0

私はこれの外観が気に入っていますが、 'key'が' tuple(a.items()) 'を返す部分について心配しています。それは別個であるが同一のdictsのために同じ順序でキーをソートすることが保証されていますか?私はdictが順序付けされておらず、同じ入力が与えられた場合に繰り返し可能な文字列を生成する 'str({1:2,3:4})'のようなものに頼っていると聞いています。 – robru

+0

'inspect.getargspec(func).args [0]'は私が尋ねた特定の質問(最初の引数の名前を見つける方法)に対する正確な答えですが、これを拡張してより一般的な解決策。私は一度それを巧みにする時間があると、後で結果を投稿します。 – robru

+0

@Robru:dictソートについての良い点。 'tuple(ソート済み(a.items()))'に変更されました(別のオプションは 'frozenset(a.items())')。 – georg

3

lru_cacheをお試しください:

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

デコMAXSIZE最新の呼び出しまで保存されます呼び出し可能memoizingと機能をラップします。高価なI/Oバウンド関数が同じ引数で定期的に呼び出されると、時間を節約できます。

lru_cacheは、Python 3.2で追加されますが、私は次のようなものをお勧めしたい2.xの

+0

について読むことが、私は、キャッシュされたインスタンスを反復処理できるようにする必要があるクラスstaticmethodsを持っているので、残念ながら私の状況では動作しませんので、キャッシュ自体はする必要がありますクラス属性として公開する。 – robru

関連する問題