毎日Learning

学んだことを共有します

エキスパート Python プログラミング メモ Vol.6

デコレータの一般的なパターンの続き。

今日はキャッシュデコレータについて。

2.3.3 キャッシュ

キャッシュデコレータは引数チェックによく似ていますが、出力が内部状態の影響を受けないというケースに限定して使用されます。

出力が内部状態の影響を受けないケースってよう分からんちん。

内部状態の影響がなければ、引数の集合から、必ず一意な結果が導き出されます。
このプログラミングスタイルは関数型プログラミングの特徴になっており、入力値の集合が有限なときに使用されます。

あー。関数型プログラミングか。いつ呼び出しても結果は常に同じっていうあれのことか。詳しくは知りませんが。

つまりそういう関数に限定して使用しようねってことか。(使用しようね…ぶふー)

キャッシュデコレータは、出力値とその出力を計算するのに必要だった引数を一緒に保管します。

へー。

2回目以降の呼び出しでは計算済みの値をそのまま返します。
この処理はメモ化と呼ばれていて、デコレータとして簡単に実践できます。

メモ化ってなんやろ。ちょいと調べてみよ。

メモ化された関数は、以前の呼び出しの際の結果をそのときの引数と共に記憶しておき、後で同じ引数で呼び出されたとき、計算せずにその格納されている結果を返す。

http://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%A2%E5%8C%96

ふむふむ。なるほど。あれか。Javaでなんかそんなデザパタがあった気がするぞ。シングルトンパターンやったかに似てるな。てかそれなのか。そうなのか。

>>> import time
>>> import hashlib
>>> import pickle
>>> cache = {}
>>> def is_obsolete(entry, duration):
...     return time.time() - entry['time'] > duration
...
>>> def compute_key(function, args, kw):
...     key = pickle.dumps((function.func_name, args, kw))
...     return hashlib.sha1(key).hexdigest()
...
>>> def memoize(duration=10):
...     def _memoize(function):
...         def __memoize(*args, **kw):
...             key = compute_key(function, args, kw)
...             # 計算済みか?
...             if key in cache and is_obsolete(cache[key], duration):
...                 print('we got a winner')
...                 return cache[key]['value']
...             # 計算
...             result = function(*args, **kw)
...             # 結果の保存
...             cache[key] = {'value': result, 'time': time.time()}
...             return result
...         return __memoize
...     return _memoize

ああ。なるほど。やはりシングルトンパターンに似ている。メモ化ってそういうことかー。へー。(って理解したけどあってるんだろうか…)

*args や **kw 引数の値を使用して SHA ハッシュキーを生成し、結果をグローバルな辞書に格納します。
ここで、ハッシュを作成するためのショートカットとしてpickleを使用しています。
引数として渡されたすべてのオブジェクトの状態をpickleによって一意なデータに変換してハッシュ化しています。
スレッドやソケットのように、適切なハッシュ化することができないオブジェクトが引数として渡されると、PicklingErrorが発生します。

ほー。pickleを使ってハッシュキーにしてんのか。これは何かで使えそうだ。

durationパラメータによって、最後の関数呼び出しから指定された以上の時間が経過したときにキャッシュされた値を無効にする機能も実装しています。

最後の呼び出しからの時間経過でキャッシュを無効にするってあたりは、シングルトンパターンと違うところやな。

関数呼び出しにおけるキャッシュ的な振舞いがメモ化っていうことか。

以下が使用例。

>>> @memoize()
... def very_very_very_complex_stuff(a, b):
...     return a + b
...
>>> very_very_very_complex_stuff(2, 2)
4
>>> very_very_very_complex_stuff(2, 2)
we got a winner
4
>>> @memoize(1) # 1秒後にキャッシュが無効になる設定
... def very_very_very_complex_stuff(a, b):
...     return a + b
...
>>> very_very_very_complex_stuff(2, 2)
4
>>> very_very_very_complex_stuff(2, 2)
we got a winner
4
>>> cache
{'c2727f43c…(なげーし意味ねーから省略)': {'value': 4, 'time': 119973…(なげーし意味ねーから省略)}}
>>> very_very_very_complex_stuff(2, 2)
4

なるほどー。しかし関数名がなんだかあれだな。まあいいや。そこはあれなんだろ。大人なあれなんだろ。

2段階ラッピングしているために、最初のデコレータの空の括弧を省略できない点に注意してください。

はーい。

高負荷な関数をキャッシュすると、プログラム全体のパフォーマンスを劇的に改善することができますが、使用にあたっては注意が必要です。

ですな。メモリリークに問題が発展しそうだわさ。

キャッシュされた値のスコープやライフサイクルの管理を、グローバルな変数で一括管理するのではなく、関数自身に関連づけることもできます。

どうやるんやろ。うーん。

しかし、どのような場合でも、より効率的なデコレータを使用するには、高度なキャッシュアルゴリズムに基づいた、特定の状況ごとのキャッシュライブラリを使用することになるでしょう。

どないやねん。でもまあ、確かに自分でキャッシュを実装しようとは思わないね。

たとえば、Webアプリケーションには分散キャッシュ機能を使用します。
Memcachedはそういった効率的なキャッシュの1つで、Pythonで使用することができます。

キャッシュについての詳しい情報とテクニックについては、13章で説明します。

いやん。13章が待ち遠しいわ。あたしまだ2章よ。

今日はここでキリが良いから3ページだけど切り上げる。

明日はプロキシデコレータだぞ!!