
「サイエンティフィックPythonのための」IDE、Spyder上にてScientific Python Lecturesの実習中。デコレータの練習つづきます。今回はfunctoolsモジュールのデコレータの「キャッシュ」で関数を包んでみます。引数が「知ってる」やつだったら、即、覚えている返り値を返すもの。
※「 ソフトな忘却力」投稿順 Index はこちら
※Sypder IDEはWindows版 6.0.8使用です。Python処理系はPython 3.11.13です。
※Scientific Python Lectures様のコースは例題だけでなく、エクササイズなども充実、それを全部順番に解いていったら必ずや立派な人になれるだろ~と思います。でも老い先短い年寄には量が多過ぎて多分死ぬまでに終わりません。適当な練習でお茶を濁してます。今回は「7.2 Decorators」の「7.2.4 Examples in the standard library」のまたまた続きです。
functoolsの中の「キャッシュ」
さて過去回でも一部練習した functools モジュールの解説ページが以下に。
https://docs.python.org/3/library/functools.html
functoolsモジュールは、「高階関数」とか出てきて何やら「高級な」雰囲気が漂っている気もしないでもないですが、過去回でもやったとおり面白い機能が目白押しです。その中で今回注目したいのは
キャッシュ (cache)
です。ハードウエアなどではお馴染みさんの「キャッシュ」ですが、ソフトウエア上の関数に対してソフトウエアでキャッシュするのが functools のキャッシュです。ぶっちゃけ、関数に与えられる引数とその時の返り値を「対」(つまりは辞書)にして覚えていき、辞書にヒットしたら関数の処理をせず、覚えている返り値を返すという仕組みみたいです。
当然、引数同じでも呼び出しの度に返り値が異なるような関数には適用できませぬ。また、二度と同じ引数で呼び出されないような関数にも適用は無駄ですな。ちょくちょく同じ引数で呼び出され、返り値を求めるのに「ある程度の」手間がかかるような関数に適用するのが吉というものでしょう。
解説ページには「階乗」関数に cacheデコレータを適用した例が載っていたので、その練習を以下に掲げます。
from functools import cache @cache def factorial(n): return n * factorial(n-1) if n else 1 factorial(10) factorial.cache_info() factorial(11) factorial.cache_info()
ここでcacheで「デコレート」された関数で使えるのが、キャッシュの塩梅を観察するための cache_info()メソッドです。キャッシュにヒットした数、ミスした数、そしてキャッシュの容量上限(Noneは指定なし)、現在使用中のキャッシュエントリ数など教えてくれます。
上記のfactorial(10)では10から0まで11回の再帰的な呼び出し全てがミス(初回だから当然)。しかし、factorial(11)では、11はミスだけれども10はヒットしてその値がキャッシュから返ってくるので、たった2回の呼び出しで結論でてます。便利ね~。
後でLRU_CACHEとの挙動の違いをみる都合上、3の階乗も求めておきます。
factorial(3) factorial.cache_info()
上記のcacheデコレータは、上限なくキャッシュに飲み込むので、1度でもやったことある引数の返り値はヒットっと。
LRU_CACHE
LRU(Least Recently Used)は、記憶容量が限られるハードウエアのキャッシュでは一番人気の、キャッシュ・エントリの管理アルゴリズムじゃないかと思います。最近使われたやつは、またヒットする確率が高いけれども、ずっと使われていないやつは「多分使われることはないんじゃね」という推測のもとリソースを回収して割り当て直すものです。
cacheデコレータもキャッシュに容量を割り当てすぎるとロクなことがない(特に返却値の長い文字列などもキャッシュ可能なので)と思われるので、制限のないcacheに対して、上限を限れるlru_cacheというデコレータも定義されとります。
lru_cacheをつかって上記と同様に階乗関数をデコレートしてみたものが以下に。
from functools import lru_cache @lru_cache(maxsize=8) def factorial(n): return n * factorial(n-1) if n else 1 factorial(10) factorial.cache_info() factorial(11) factorial.cache_info()
結果は以下の如し。ここまででは cache と挙動の差はありませぬ。
しかし、以下の factorial(3)を実行してみると、maxsize=8とエントリ数を制限した効果が現れます。
factorial(3) factorial.cache_info()
maxsize=8のために、factorial(3)の時点では、すでに忘れてしまっているみたい。そのため、再度3から階乗を計算、覚え直しているみたい。
記憶容量を制限せずにキャッシュするのか、記憶容量をコントロールしながらキャッシュするのか、プログラマ様の思し召し次第だと。