Practical Python Design Patterns - The Proxy Pattern 編
Practical Python Design Patterns - Python で学ぶデザインパターン: The Proxy Pattern - Part. 1 「第9章: プロキシパターン」の巻 投稿一覧へ戻る
Published 2022年6月10日8:28 by mootaro23
SUPPORT UKRAINE
- Your indifference to the act of cruelty can thrive rogue nations like Russia -
Chaper 9: Proxy Pattern(第9章: プロキシパターン)
Do you bite your thumb at us, sir?
(我々に向けて親指を噛んでいるんですかい、紳士殿? [相手を侮辱する/相手に喧嘩を売るジェスチャー])
Overview
(概要)
プログラムが大きくなるにつれて、頻繁に呼び出す関数の存在に気付く場合があります。もしその関数の実行が非常にコストの高いものである場合、プログラム全体のパフォーマンスに影響が及ぶのは避けられません。
n 項のフィボナッチ数を算出する次の例を考えてみてください:
simple_fib.py
def fib(n):
if n < 2:
return 1
return fib(n - 2) + fib(n - 1)
if n < 2:
return 1
return fib(n - 2) + fib(n - 1)
この非常に単純な再帰関数は深刻な欠陥を抱えています、特に n の値が非常に大きい場合は。それは何でしょうか?
このコードでは、項目数が 2 よりも大きなフィボナッチ数を計算する際には fib(x) という計算が何回も何回も繰り返される必要がある、という事実に気付けたでしょうか。
この類の問題を解決する手段は複数ありますが、ここでは「メモ化 (memoization)」と呼ばれる少し特殊な方法を採用したいと思います。
Memoization(メモ化)
ある関数を同じ引数で何回も呼び出す場合、その関数からの返却値を保存しておけば、再度その関数を呼び出して全ての処理を繰り返し行うことを防止することができます。その関数への呼び出しがあった場合、実際にその関数を呼び出すのではなく保存しておいた値を返すわけです。このように、ある関数へある引数を渡して呼び出した結果返された値を保存しておき、2回目以降の同様の呼び出し時に利用する操作を「メモ化 (memoization)」と呼びます。
この「メモ化」の効果がどれほどのものなのか、先程のシンプルなフィボナッチ数取得関数で確認してみましょう。まずは最初の例を複数回実行するのに要する時間を計測してみます:
profile_simple_fib.py
import time
def fib(n):
if n < 2:
return 1
return fib(n - 2) + fib(n - 1)
if __name__ == '__main__':
start_time = time.perf_counter()
fib_sequence = [fib(x) for x in range(1, 41)]
end_time = time.perf_counter()
print(f'{len(fib_sequence)} 回のフィボナッチ数算出所要時間: {end_time - start_time} 秒')
def fib(n):
if n < 2:
return 1
return fib(n - 2) + fib(n - 1)
if __name__ == '__main__':
start_time = time.perf_counter()
fib_sequence = [fib(x) for x in range(1, 41)]
end_time = time.perf_counter()
print(f'{len(fib_sequence)} 回のフィボナッチ数算出所要時間: {end_time - start_time} 秒')
あれっ、結果が返ってこない!?、と焦った方もいるのではないでしょうか?私のパソコンで実行した結果は以下のようなものでした:
40 回のフィボナッチ数算出所要時間: 52.272749300000044 秒
もしシステム構成が「バカッ速」であればこの結果は鼻で笑われてしまうかもしれません。そんな時は、400 回、4000 回 ... と項目数を増やして試してみてください。そして、あなたのマシンで「永遠に終わらない」回数ではなく「程よく時間がかかる」回数を見つけてください。この章の残りのサンプルコードでは、見つけたその「程よく時間がかかる」回数で試すようにしてください。
では、各呼び出しの結果をキャッシュした場合、これがどのように実行時間に影響を与えるのか見てみましょう。
今回のフィボナッチ数算出関数では、計算済みの n 項のフィボナッチ数を辞書に保存しておき、同じ項数呼び出しに対しては計算することなくそのキャッシュ値を返すことにします:
import time
def fib_cached1(n, cache):
if n < 2:
return 1
if n in cache:
return cache[n]
cache[n] = fib_cached1(n - 2, cache) + fib_cached1(n - 1, cache)
return cache[n]
if __name__ == '__main__':
cache = {}
start_time = time.perf_counter()
fib_sequence = [fib_cached1(x, cache) for x in range(1, 41)]
end_time = time.perf_counter()
print(f'{len(fib_sequence)} 回のフィボナッチ数算出所要時間: {end_time - start_time} 秒')
def fib_cached1(n, cache):
if n < 2:
return 1
if n in cache:
return cache[n]
cache[n] = fib_cached1(n - 2, cache) + fib_cached1(n - 1, cache)
return cache[n]
if __name__ == '__main__':
cache = {}
start_time = time.perf_counter()
fib_sequence = [fib_cached1(x, cache) for x in range(1, 41)]
end_time = time.perf_counter()
print(f'{len(fib_sequence)} 回のフィボナッチ数算出所要時間: {end_time - start_time} 秒')
実行結果は以下のようになりました。パソコンを速いものに交換したわけではありませんよ:
40 回のフィボナッチ数算出所要時間: 1.6400001186411828e-05 秒
大変良い結果が得られましたので、フィボナッチ数を含むその他いくつかの数学計算を行う「計算オブジェクト」を作成したいと思います:
class Calculator:
def fib_cached(self, n, cache):
if n < 2:
return 1
try:
result = cache[n]
except:
cache[n] = self.fib_cached(n - 2, cache) + self.fib_cached(n - 1, cache)
result = cache[n]
return result
...
def fib_cached(self, n, cache):
if n < 2:
return 1
try:
result = cache[n]
except:
cache[n] = self.fib_cached(n - 2, cache) + self.fib_cached(n - 1, cache)
result = cache[n]
return result
...
このクラスを利用するユーザーの大多数は cache 引数の存在理由や扱いに戸惑うはずです。理想を言えば、ユーザーからはこの章の最初に実装した通常関数のようにこの fib_cached() メソッドが扱え、しかし内部的にはキャッシュによるパフォーマンス向上を享受できるようにしたいわけです。
これを実現するために考えられる方法の1つは、Calculator クラスのインターフェースとして機能するクラスを定義することです。しかし Calculator クラスのユーザーはこのインターフェースクラスの存在を認識せず、あくまでもオリジナルの Calculator クラスのインターフェースに対してコードを記述するだけです。しかし実際には、オリジナルクラスと共通のインターフェースを備え同じ機能を提供するプロキシクラス (proxy) に対するリクエストとして内部的には処理を行います。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -