お久しぶりです、コウスケです。
前回はクロージャを紹介しました。
今回はクロージャのもう少し具体的な使用例を挙げていきたいと思います。
クロージャは関数型プログラミングの基本概念なので、
それ単体でどう使えるかというよりも、それがあるから
様々な機能が実現できる、というものになるため、
それらを紹介していきます。
まず、あげられるのが関数の部分適用(カリー化)です。
これは複数の引数を持つ関数に対し、一部の引数を固定した関数を作ることができます。
サンプルコードを上げると次のようになります。
[サンプルコード]<br />import sys<br />def output_stdout_file(f, log):<br /> """<br /> ファイルオブジェクトfと標準出力両方に対して文字列logを書き込む関数<br /> """<br /> sys.stdout.write(log+"\n") # 標準出力<br /> f.write(log+"\n") # ファイル出力<br /><br />def curried_output_stdout_file(f):<br /> """<br /> output_stdout_fileを部分適用し、<br /> 第一引数f(ファイルオブジェクト)を固定する関数<br /> """<br /> print "ファイル%sに出力"%f.name<br /> <br /> def _curried_output_stdout_file(log):<br /> output_stdout_file(f, log)<br /> return _curried_output_stdout_file<br /><br />def main():<br /> fa = file("logA.txt", "w")<br /> write_log_a = curried_output_stdout_file(fa)<br /> write_log_a("logA_1")<br /> write_log_a("logA_2")<br /><br /> fb = file("logB.txt", "w")<br /> write_log_b = curried_output_stdout_file(fb)<br /> write_log_b("logB_1")<br /> write_log_b("logB_2")<br /><br /> write_log_a("logA_3")<br /> write_log_b("logB_3")
[実行結果] ・標準出力 ファイルlogA.txtに出力 logA_1 logA_2 ファイルlogB.txtに出力 logB_1 logB_2 logA_3 logB_3 ・logA.txtの中身 logA_1 logA_2 logA_3 ・logB.txtの中身 logB_1 logB_2 logB_3
この例ではまず、output_stdout_fileという、
ファイルオブジェクトとログ文字列を2つ指定して、
ファイルと標準出力に両方共出力する関数が既に用意されているとします。
これの引数に毎回ファイルオブジェクトを指定すると煩わしいので、
指定のファイルオブジェクトに固定して
引数を一つ減らした関数curried_output_stdout_fileを作ります。
こういった操作を関数の部分適用(カリー化)と呼びます。
後は、作った関数write_log_a, write_log_bを適宜呼ぶことで
指定のファイルオブジェクトに書き込まれていくという仕組みです。
※備考ですが、関数を部分適用する関数は
functools.partial という標準ライブラリ関数で簡単に作成することができますので
興味があれば調べてみてください。
また、前回紹介したとおりクロージャは最初に作成されたときの
引数などの環境を覚えているので、
「初期化機能を持った関数」をつくることができます。
例えば、先ほどのcurried_output_stdout_file を、
「ファイル名を渡すようにしてファイルオブジェクトの作成を行う」
というところまでを初期化で行うようにすると、次のように作成できます。
def output_log(name): # 初期化 f = file(name, "w") print "ファイル%sに出力"%f.name def _curried_output_stdout_file(log): output_stdout_file(f, log) return _curried_output_stdout_file
これを、
write_log_a = output_log("logA.txt") write_log_b = output_log("logB.txt")
とすれば同じように使用できます。
次に、関数合成を紹介します。
これは、「任意の2つの関数を順次適用させた関数」を作成できます。
例として、次のabs_float関数は数値文字列を数値として解釈し、その絶対値を返します。
[サンプルコード] def compose(outer_f, inner_f): """ 合成関数を返す関数 """ def _compose(x): return outer_f(inner_f(x)) return _compose def main(): abs_float = compose(abs, float) #abs, floatは組み込み関数 print "abs_float: ", abs_float("-5.3")
[実行結果] 5.3
これは渡された文字列に対しfloat, absを順次適用、合成させる関数composeにより abs_float関数を作成することにより実現されています。 また、このcompose関数は汎用的なので一引数の任意の二つの関数を合成することができます。
最後に、メモ化関数を説明します。 メモ化とは、簡単に言うと
「関数の実行結果をキャッシュしておいて、同じ引数で指定されたら キャッシュされた結果を返す」
仕組みです。
[サンプルコード] def memoize(obj): """ メモ化関数 """ cache = obj.cache = {} # 関数の結果キャッシュ格納関数 def _memoize(*args, **kwargs): if args not in cache: cache[args] = obj(*args, **kwargs) else: print "ファイル%sのキャッシュを使用"%args return cache[args] return _memoize def read_file(name): """ ファイル名nameを読み込んでテキスト文字列を返す """ print "ファイル%sを読み込み"%name f = file(name, "r") content = f.read() f.close() return content def main(): memoized_read_file = memoize(read_file) text = memoized_read_file("logA.txt") text = memoized_read_file("logA.txt") text = memoized_read_file("logB.txt") text = memoized_read_file("logB.txt") text = memoized_read_file("logA.txt")
[実行結果] ファイルlogA.txtを読み込み ファイルlogA.txtのキャッシュを使用 ファイルlogB.txtを読み込み ファイルlogB.txtのキャッシュを使用 ファイルlogA.txtのキャッシュを使用
このように既に処理された関数はキャッシュされた結果を使用していることがわかると思います。
また、このmemoize関数は任意の関数に対して適用することができます。
これらの例で、「少しは関数型プログラミングも便利だな」と感じることがあったのではないでしょうか?
今回のこれらの例は基本的に「既存の関数に機能を付け加えて新たな関数を返す」という
仕組みになっています。こういった機能を「デコレータ」と呼び、様々な用途に応用できます。
(比較的有名なのはjavaのInputStream APIです。)
pythonではデコレータを簡単に表記する構文も用意されているので、
また別の機会に様々な例を紹介できればと思います。
(実際、今回紹介した関数郡も実際はデコレータを用いて記述することが出来ます。)
[今回のまとめ]
・クロージャ機能により、部分適用、関数合成、メモ化など様々な機能が実現できる。
・関数に機能を付け加えて新たに関数を返すことをデコレータと呼び、様々な用途に応用できる。