前回、Pythonのデコレータを紹介した。
繰り返すが、機能的な話をすると、デコレータとは構文でも何でもなく、関数の記述形式だ。具体的には関数Aを受け取り、関数Bを返す高階関数と言う「スタイル」がPythonでのデコレータだ。
一方、Webで検索すると、@何とやらと言う記述がデコレータと呼ばれている。要は「関数定義を修飾(デコレート)してる」から、@何とやらをデコレータと呼ぶ、と。
しかし、前回解説した通り、実の事を言うと@何とやらは別に関数定義を修飾してるわけじゃない。実際は@何とやらそのものはむしろ関数定義であるdef構文の拡張で、それ自体が関数定義のキーワードとして働いている。そして@以下で記述するdefはフツーに関数定義で使っているdef構文ではなく、@構文(と呼ぼう)の構文要素だ、と言う話をした。つまり、「このdef以下を纏めて関数として解釈しますよ」と言うサイン、あるいはキーワードであり、関数定義自体は@自身が行ってるんだろう、ってのがその概要だ。
まぁ、確かに本当にそうだかは知らん(笑)。Pythonのソースコードを読んだわけじゃねぇしな。ただし、Lispでのマクロ挙動を考えると少なくともそう解釈出来る、って事だ。
いずれにせよ、ここでは、@構文は「関数定義を修飾してる」のではなく、関数定義そのものを行っていて、と同時に、関数名と同名の変数を自動生成したあと、こっそりその関数オブジェクトをデコレータでデコった後にその変数へと自動で代入してる、とする。
さて、前回指摘したが、Web上のデコレータの例示、なんつーのはこんなんばっかだ。
def foo(func):
def wrapper():
print("hoge")
func()
return wrapper
@foo
def bar():
print("fuga")
で、barを実行するとこうなる、と。
>>> bar()
hoge
fuga
クソの役にも立たん例だ(笑)。
しかも、おっそろしい事に、基本的にどのサイトあるいはブログを眺めてもオチは「Django の @login_required」が実例です、となっている(笑)。
当然、上のprint塗れのデコレータを書いてみたトコで、実用的なデコレータを書けるわけでもないし、このテの記事を書いてる人々が、「デコレータを日々書いて使っている」わけでもないんだろう。また、言っちゃえば無理して書くべきものでもないし、また書けるようなモノでもない、んだ。
別にバカにしてそう言ってるわけじゃない。単にデコレータは抽象化レベルが高い機能だからだ、って事であり、またそもそも高階関数とはそういうモノで、ポッと出でそうそう「作るべきもの」を思いつくわけでもない。
そもそも、プログラミングに於いて、まず何百回も同じようなコードを書いて、いやんなっちゃって、「頻出する関数」を纏めてユーティリティを作ろう、とするわけだろ。そこさえも面倒臭いんで、Pythonのような言語だとBattery Includedになってるわけだが、一方、基本的に「ほぼ何もユーティリティが存在しない」C言語とかだと自作ユーティリティを作って溜め込まざるを得なくなる。具体例だと例えば「線形連結リスト」とかだよな。プログラムを書いてる内に毎回毎回「線形連結リスト」を書く必要が出てくれば、よっぽどのトンマじゃない限り、「いい加減これをライブラリ化しよう」とする筈だ。
他にも、例えばしょっちゅう数値を比較しては大きい方を代表する数を使うコードが出てくるとする。手間から言えば別に比較演算子で直接比較してもいいんだけど、毎回毎回そればっか出てきては書いてるような気がする、と。しかも3つ比較したり4つ比較したり、とか様々なケースがありえるわけだが、いい加減こんなのも纏めて何とか出来ないか、と。
そういう時が「個人的なユーティリティライブラリ」にその関数を追加するチャンスなわけだ。幸いにもPythonにはmax関数があるけど、無かったら自分で作ってライブラリ化するわけだ。
例えばPythonだとこんな風に書くだろう。
def my_max(*args):
tmp = args[0]
args = args[1:]
while args:
tmp = tmp if tmp > args[0] else args[0]
args = args[1:]
return tmp
そもそも、こうやって「処理を関数に纏められる」と言うのが登場時の構造化プログラミング言語のメリットだった。「処理を関数に纏められる」すなわちプログラミングでユーティリティを作りやすく、プログラムはユーティリティの組み合わせになる、と。
ここで抽象化の階段を一つ登ったわけだ。
そして、Lispを初めとする関数型プログラミング言語はこれだけでは飽き足らず、抽象化の階段をもう一歩登ろうと画策したわけだ。
上の「構造化プログラミング」の基本的なアプローチは「足りない関数は自作せよ」だった。一方、Lispを初めとする関数型言語がそれに付け足そうとしたのは
「プログラムを書く際に出てくる記述の"頻出パターン"そのものをライブラリ/ユーティリティ化出来ないか?」
だったんだ。
例えば上のmy_max関数の例だと、「繰り返し」は頻出パターンで、ここは構文として用意されている。一方、my_maxは与えられた引数の内、最大値を返す関数として固定されてはいるが、この「プログラムを構成してる」パターン自体が頻出じゃないのか、と考えて、そこを抽象化すれば?って考えたわけだ。これで出てくるのが高階関数、なんだ。パターン自体を抽象化してしまえば、似たパターンを書かなきゃならない時、記述パターン自体を抽象化した関数を使えばいい。
例えば上のmy_maxは高階関数functools.reduceを用いれば次のように書ける。
from functools import reduce
def my_max(*args):
return reduce(
lambda init, tmp: init if init > tmp else tmp, args[1:], args[0]
)
ANSI Common Lispの場合、全部入りマシマシ言語を目指していたが、Schemeの場合はこの「ユーティリティが何を目指してるのか」が割にハッキリしている。Schemeは特に「非実用言語」の代表格のように思われてるが、実の事を言うと、Schemeのユーティリティは「何が実用上必要か」ではなく、プログラミングにおける「記述の頻出パターンに何があるか」を重視しているように見えて、結果そのテのライブラリをビルトインとして提案してるように思う。
結果、Schemeが小さいのは、ある意味「頻出の記述パターン」を厳選していくとあの量になる、と言う恐るべき事実の裏返しのような気がする。言っちゃえば、Schemeは、例えば日本人向けに書かれた「超頻出英語構文、これさえあれば貴方もネイティヴのように話せます! 50選」みたいな本に近い仕様になってんだ(笑)。
いずれにせよ、関数型言語が考えた「ユーティリティ」ってのは「プログラムを書く上での頻出パターン」の抽象化で、これで抽象化の階段をもう一段登ったわけだ。そしてそのテのユーティリティは今のトコ、そんなに数は多くない(※1)。
そう、そもそも、ユーティリティとして見た場合、汎用的に使える高階関数の数がそもそも多くない、んだ。結果、高階関数として利用出来る汎用的なデコレータの存在数も(現時点で発見されてるモノ以外)そう多くはない、と言う事が言える。
言い換えると、筆者の技術的な力量は問わないが、最初の例、
def foo(func):
def wrapper():
print("hoge")
func()
return wrapper
@foo
def bar():
print("fuga")
と言うショボいパターンが頻出なのは何故か、と言うのは、そもそも「デコレータ」としてデザイン出来る関数の上手い例を思いつけないから、なんだよ。
繰り返すが、これはこのテの記事の記述者達の力量がどーの、ってのはあんま関係ない、ってば関係ないんだ。
そもそも、「デコレータ」と言う記述が何故に必要なのか、と言うと、「色んな関数をデコレート出来る」と言う前提があるからだ。そこに「汎用性」がある。
しかし、例えばここで、
@foo
def baz():
print("piyo")
みたいな関数bazを書けて嬉しいか?って問題がある。
>>> baz()
hoge
piyo
なるほど、確かに関数bazもデコられている。
しかしそもそも、このパターンの場合、「わざわざある関数を別の高階関数でデコる」より、
def func(s):
print(f"hoge\n{s}")
と言う風に書いた方がマシなんだ(笑)。フツーの関数で十分抽象化は成されている。
繰り返すが、汎用的なデコレータの例をパッと思いつく方が難しいんだよ。
それがPython公式が「デコレータの作り方」を敢えて教えてない遠因だろう。デコレータ、ってのが「特定のある関数をデコるだけ」だとしたらそんなモンは意味がない。だったら最初から単一の関数として設計すべきだ。
かと言っていきなり「汎用的なデコレータ」を設計するのは至難の業だ。そんなモン、コンピュータ・サイエンスの辞書的な本を探してみてもあるのは数件、程度だろう。
そしてその数件程度、ならPython公式が既にPythonに組み込んでいるだろう。
ユーティリティとしての高階関数、なんつーのはマジで「思いつこう」として思いつけるようなモンじゃないんだ。
貴方が何百、何千、と言ったプログラムを書いていって、嫌になって(笑)、「あれ、この記述パターンは良く見かけるな」とか「またこのパターンが出てきたよ」となったとき、既存のコードから共通パターンを見出した時、それは成立する。
前からプログラミングには大して論理性は必要ない、と言ってきた。むしろ必要なのはパズル好きか否か、って辺りで、以前書いたけど、何らかの共通パターンを見いだせて括り出せて嬉しい、ってなった時がデコレータを書くべき時、なんだ。
一朝一夕でデコレータを書ける必要はない。もうホント「同じパターンが頻出して嫌んなっちゃった」時がその「共通パターン」を括りだしてデコレータに纏めてしまう、ってのが正しい使い方、なんだよ。
なお、「汎用のデコレータを書いてみたい」と言う野望を持ってる人にはこの文書をまずは読んでみる事をオススメする。
ただし残念ながら、デコレータを作れる、って事と@構文を使うべきだ、ってのは必ずしも一致しないんだ。特にPythonに於いてはそう言える。何故なら大きな影響を受けてはいるが、Pythonは関数型言語じゃないから、だ。
例えば、パッと思いつくデコレータ(関数Aを受け取り関数Bを返す)として簡単なモノにコンプリメント(補完)関数がある。
def complement(fn):
return lambda *args: not fn(*args)
これはPythonで言うトコのデコレータの定義、「関数Aを受け取り関数Bを返す」を満たしている。
しかし、Pythonの(Lispユーザーから見た)欠点にPythonには述語が少ないと言う事が挙げられる(※2)。上のコンプリメント関数は単純に言うと「真偽値を返す関数」の挙動を真逆にするわけだが、生憎Pythonには実はそのテの、関数型言語では必須と言える関数が少ない、かあるいは無い。
例えばリストが空リストかそうじゃないか、と判定する場合、Lispなら例えば
> (null? '())
#t
で判定するが、一方Pythonではこうだ。
>>> [] == []
True
Pythonでの基本は何でもかんでも == で判定する。もちろん、Pythonでも
def is_null(arg):return arg == []
と言うような関数を書けないわけではないが、ビルトインじゃない述語をわざわざ作ってコンプリメント関数を使う、なんつーのは利便性で考えるとイマイチなんだ。
ホント、Lisp観点から言うと述語の少なさ、ってのはPythonの欠点なんだけど、実用上問題ない、って歯がゆい感想があるんだ。奇数、偶数判定するにも==や!=で判定して、実用上は全く問題がない。一方、そういう構成はデコレータのような高階関数を使う場合、別に関数定義をせなアカン、ってぇんで利便性が大幅に落ちるんだよ。
# 奇数を判定する関数を作ってdef is_odd(x):
return x % 2 != 0# わざわざコンプリメント関数でis_evenを成立させる?
@complement
def is_even(x):
return is_odd(x)
これってどうなんだろうな、ってのがある。
しかもis_even関数の定義も分かりづらいだろう。返してるのはis_odd関数を使った結果であり、これで何故に結果が反転するのか(もちろんコンプリメント関数の作用だが)、パッと見全然意味が分からないだろう。
また、Lisp使いは「関数名を必ず決めなきゃならない」と思い込むような習慣がない。
例えば同じコンプリメント関数を使うなら、わざわざ@構文を使って「名前を付ける」なんつー事もやらんだろう。
例えば、リスト[1, 2, 3, 4, 5, 6]から偶数要素を削除する、ってぇのなら、「コンプリメント関数を使うなら」次のように書くだろう。
>>> [i for i in [1, 2, 3, 4, 5, 6] if complement(lambda x: x % 2 == 0)(i)]
[1, 3, 5]
「この程度」ならこうやって直接書くのを好むだろう。と言うのもLisp使いは無名関数に慣れてるから、だ。「関数は名前を付けなければならない」と言う強迫観念がそもそも存在しない。
一方、Python慣れしてる人なら上の表現が、「デコレータ使用」の割には明らかに「冗長だろ」って感想を抱くとは思う。それも当然だ。
なんせPythonだったらこう書いて構わないから、だ。
[i for i in [1, 2, 3, 4, 5, 6] if not i % 2 == 0]
[1, 3, 5]
こっちの方が遥かに短く記述出来る。
そしてこれが成立するのは、Lispではnotは関数だが、一方、Pythonではnotが構文だから、だ。
余談だが、Pythonは「記述が簡単になるように」たくさん構文が用意されている。言い換えると「記述上の例外がたくさん生じるように」多くの構文が必要になってる、って事だよな。そして記述パターンは実装者が決めていて、そこからはみ出すような記述は受け付けない。記述は簡単に見えるけど、割に厳格に決められているんだ。
反面、Lispの場合は、表面上でも仕様上でも構文が少ない。いや、一般的な意味で言うと「構文が無い」。言い換えると、前置記法しかないし、表面上の記述の簡易さを成立させる為だけの「構文」なんかは基本用意しない方針なんだ。
結果、「Lispが柔軟性が高い」と言われるその理由は、構文の数をなるたけ排し、出来るだけ「関数ならば同様に扱えるように」関数である事の比率を上げているからだ。多少書きづらくても、「一貫性」の方を重視してるお陰で、「関数なら同じように扱える」と言う枠組みを外していないんだ。
事実、例えばSchemeだと、フツーのプログラミング言語での脱出構文、breakにあたるようなcall/ccは単なる関数だ。
結果、実用性は全く無いが、こういうバカな事も出来てしまうんだ。
> (map call/cc `(,(lambda (break) (break 1)) ,(lambda (break) (break 2)) ,(lambda (break) (break 3))))
'(1 2 3)
>
全く意味がないが、「call/ccが関数である以上」フツーにリストにマッピングしても「文法違反」なぞにはならない。何故なら「脱出機構でさえ」Schemeでは構文じゃなくって関数だから、だ。単なる関数である以上、他の関数と同様にマッピングも出来る。
一方、notが構文なPythonだと次のような記述は受け付けない。
;; Lispで可能なこのような記述は> (map not '(#t #f #t))
'(#f #t #f)
# Pythonだと許されない>>> map(not, [True, False, True])
SyntaxError: invalid syntax
もちろん、mapを使わずにリスト内包表記だとLispのmapを使ったような結果を得る事は許される。
>>> [not i for i in [True, False, True]]
[False, True, False]
ただし、これはリスト内包表記はmapのような関数ではなく、構文である事が理由としては大きく、構文の中での構文の使用例のパターンってのが実装者によって厳密に決められてて、その例に則ってるから、に他ならない。
いずれにせよ、Pythonは「記述規則の例外」をたくさん抱えていて、そのお陰で「記述自体は簡便になるように」設計されているが、一方、色んな「機能」を統一的に使うのは結構難しいんだ。
そしてそれが意味する事は、「記述が簡単なPython」が「覚える事が少ない」事を必ずしも意味しない。むしろ「何が関数で何が構文なのか」キチンと分けて覚えないとならないし、構文なら「用法」もキチンと確認しとかなアカンわけだ。
加えると、デコレータのような「高階関数」だと、「関数が多い言語」なら汎用的に使えるが、「構文が多い言語」ならそこまで万能性がない、ってのは上の例で見た通りだ。
結果、簡単なデコレータの「説明としての」モデルとしてはコンプリメント関数は有用だが、残念ながら構文比率が多いPythonだと適用範囲はLispより小さくなってしまう。そしてPythonに明るい人なら、上の例だとコンプリメント関数のようなデコレータを使うよりこう書いた方が遥かに明解だ、って事に同意するだろう。
# 実は中置記法の != なんかも構文的な要素だ>>> [i for i in [1, 2, 3, 4, 5, 6] if i % 2 != 0]
[1, 3, 5]
さて、こう考えてくると、前回扱ったメモワイズ関数が、デコレータとして如何に優秀な例だったのか分かると思う。まずは設計そのものが元々「別の関数をデコる」と言う目的を明らかに持っている事。もう一つは@構文を使って「名前を付けた関数にする」必然性があった。メモワイズ関数でデコった関数は、性質的に、「何度も使われる」前提なんで、「名称を付ける」必然性がある。メモワイズ関数は無名関数的に使うには全く向かない、んだ。
そして、高階関数は関数型言語から来た機能だが、一方、メモワイズ関数自体は関数プログラミングから逸脱している。何故ならメモワイズ関数は「状態」を保持してるから、だ。
def memoize(fn):
cache = dict() # ここで「状態」を保持している
def wrapper(*args, **kwds):
try:
return cache[args]
except KeyError:
cache[args] = fn(*args)
return cache[args]
return wrapper
メモワイズ関数はローカルで定義されてるwrapper関数を返してる。そしてPythonの関数スコープは関数が定義された時の「環境」を記憶している。従って、ローカル環境で定義されたwrapper関数は、wrapper関数がどういう環境内で定義されたのか覚えていて、結果、cacheと言う辞書型が環境内に「あった」と言う情報も覚えているわけだ。
こういった「定義された環境を覚えている」関数をクロージャと言う。
そして、クロージャはスコープにより自然と形成されはするが、純粋に「関数プログラミングなのか?」と問われると極めて怪しい(何せ、そもそも「辞書」にデータ登録をする事も「破壊的変更」だ・笑)。実際、メモワイズ関数は純粋関数型言語であるHaskellなんかでは作りづらい関数だろう。恐らく辞書型(ハッシュテーブル)の代替として平衡二分探索木を用い、悪名高いIOモナドの一種、IOExtsを利用しないとどーにもならんのじゃないだろうか。
しかも、Haskellは変数の再代入は許さないだろう(※3)。
いずれにせよ、実はメモワイズ関数自体は、ポール・グレアムが挙げたアキュムレータと形式を共有している。
def foo(n):
def bar(i):
nonlocal n # この宣言でfooが作り出す環境内で、引数nが「状態」だと指定する
n += i
return n
return bar
メモワイズ関数だと「状態」の存在が計算プロセスを変更してるし、また、結果、「メモワイズ関数でデコられた」関数の@構文による「名前付け」の必然性を生み出している。
これによる「デコレータを作成するチャンス」へのヒントは、「内部的に状態を保持する関数」がデコレータになり得る、と言う事であり、また@構文による「名前付け」が役立つ例になる、って事だ。
まるで@構文はメモワイズをする為に作られたように見える程、だ。
と言う辺りで本題に入ろう。
ところで、繰り返すが、機能的には、Pythonで言うデコレータとは「関数Aを引数に取り関数Bを返り値とする」関数の事だ。
ただし、ちと補足が必要だ。もうちょっと言うと関数A以外には引数を取らないと言う条件がある。
例えば関数プログラミングには関数fと関数gの二つを引数に取り、関数f○gを返す関数、なんつーのもある。これを合成関数(compose)と言う。
def compose(*fns):
if fns:
fn1 = fns[-1]
fns = fns[:-1]
return lambda *args: reduce(
lambda init, fn: fn(init), reversed(fns), fn1(*args)
)
else:
return lambda *args: args
いや、合成関数って厳しい名前なんだけど、恐らく高校数学で見た事あるんじゃなかろうか。しかも名前は厳しいけど、(f○g)(x) = f(g(x))と○の右側に書かれた関数から左へと連続適用していくだけ、だ。
見た通り、composeは合成関数f○gを返す。そしてそれ自体に引数を適用すると値を返す。上の例だと引数1を与えて、
- lambda x: 1 + xと言う関数に従って2が返る。
- 1. の結果に対しlambda x: [x]に従ってリスト化する。
結果、引数である関数は右から順に適用されて「合成関数の定義が要求する通り」の結果を得るわけだ。
しかし、まぁ、Lisp使いはあまり気にせんのだけど、composeの書式が怖い、って人もいるだろう。「@構文で名前を付けられるデコレータの形式にしたい」と。
ところが、ここで先程の制限に突き当たるんだ。Pythonでのデコレータは原則、「関数一個しか引数を受け付けない」と。一方、上の合成関数は「関数が2個(以上)引数として取られてる」。
こういう場合一体どうすんの?と。そして一般に、デコレータで「関数以外にも引数を取りたい」場合、一体どう書けばエエの?と。
まぁ、ぶっちゃけ、composeはcomposeのままで使った方がいい、んだけど、ここでは練習として、デコレータ自体がどうしても複数の関数を引数に取る、とか、関数以外の引数を要する場合どうするのか、と言う事を論じる。
前回見た通り、デコレータの雛形は次のようなモノだ。
def decorator(fn):
...
def wrapper(arg0, ...):
...
return wrapper
デコレータが引数を取りたい場合、記述形式の雛形は次のようになる。
def decwrapper(Arg0, ...):
...
def decorator(fn):
...
def wrapper(arg0, ...):
...
return wrapper
return decorator
単純に言うと、「基本のデコレータ」を引数付きの関数でデコればいい。
そうすれば、@構文に対して引数付きのdecwrapperを指定するとそこが展開されて内情は関数fnを引数とする関数decoratorになる。
もうちょっと細かく説明すると、ローカル関数decoratorはやはりその「定義時の環境の情報」(つまり、外側にあるArg0、...等の引数の「状態」)を記憶するクロージャとして返される。
ちょっと抽象的な説明だろうから、実際composeを@構文でも使えるように書き替えてみよう。
def compose(*fns):
def _compose(func):
def wrapper(*args, **kwds):
return reduce(
lambda init, fn: fn(init), reversed(fns), func(*args)
) if fns else args
return wrapper
return _compose
機能的には@構文で定義した関数をcomposeの引数で取った関数群に適用する場合と@構文で定義した関数をcomposeの引数で取った関数群で畳み掛ける2種類が考えられるが、ここでは当然後者を採用してる。じゃないと意味的に@構文で定義した関数がcomposeを逆にデコレートしてしまうから、だ。
上でも書いたが、上のcomposeの定義式では実際のデコレータは_composeで、これはクロージャで外側のcomposeの引数(関数群*fn)を環境情報として記憶しつつ@構文で展開される。
例えば@構文で次のように書くと、
@compose(lambda x: [x])
def list_add1(x):
return 1 + x
関数list_add1は引数に1を足した結果をリスト化し返してくれる。
>>> list_add1(1)
[2]
composeのデコレータを利用した記述法は他にも考えられる。デコレータ自体をデコレータでラップする、と言う方針だ。具体的にはオリジナルのcomposeの記述自体を@構文で作成する、と言う事だ。
def kompose(func):
def wrapper(*fns):
def _wrapper(fn):
def __wrapper(*args, **kwds):
return func(*fns, fn)(*args)
return __wrapper
return _wrapper
return wrapper
@kompose
def compose(*fns):
if fns:
fn1 = fns[-1]
fns = fns[:-1]
return lambda *args: reduce(
lambda init, fn: fn(init), reversed(fns), fn1(*args)
)
else:
return lambda *args: args
デコレータkomposeは4段もdefがあるが、まぁ、こういった事も可能だ、と言う例に過ぎない。
デコレータkompose自体はフツーのデコレータとして働き、@komposeの記述によって以下に定義された関数をデコる。しかし、そのデコられた関数(compose)は結果、多引数をもったクロージャ(wrapper)の情報を持っていて、composeがデコレータとして呼ばれた際に、今度は内側の_wrapperがクロージャとして返され、更に内側のクロージャ、_wrapperが真のデコレータとして機能する。
かなりややこしく感じるだろうが、「クロージャが一体何を返すのか」丁寧に考えていけば読み解く事自体は然程難しくない。また、最初に書いた通り、本来ならcomposeはフツーに高階関数として使われた方が使い勝手がいい筈で、ここでPythonで言う「デコレータ形式」で書いたのはあくまで「練習の為」、っつーか、Web上に参見する「ショボい例」よりゃマシだろう、ってぇんでかなり無理して書いた例に過ぎない、んだ。
他にはここで見た数値積分、なんかがPythonの「引数付きデコレータ」の良い例になるかもしれない。
from math import sqrt
def integrate(a, n):
def decorator(f):
def wrapper(b):
delta_x = (b - a) / n
return sum([f(a + i * delta_x) * delta_x for i in range(n)])
return wrapper
return decorator
「引数付きデコレータ」の引数部分には積分の開始地点、そして区間分割数を与える事とする。
デコレータ本体はセオリー通りに「受け取る関数」を指定、そしてその内側のローカル関数wrapperでは「積分の終了地点」を引数として指定し、あとはその本体で数値積分を実行させるわけだ。
で、@構文で「積分したい関数」を記述すれば、その時点でその関数の原始関数を利用した積分計算を行う関数を定義するのと同義、となる。
@integrate(0, 10) # 開始地点が0、分割数10を指定
def integrate_square_x(x):
return sqrt(x)
integrate_squareを実行すれば、0から任意のxまでの10分割の数値積分が行われてる事が分かるだろう。
>>> integrate_square_x(1)
0.6105093417068175
確かに数値計算はニッチと言えばニッチだが、一方、「デコレータ」を実験するには、いっつも書いてるが、「数値計算」と言うのは割に簡単で良い題材なんだ。
そして「積分」自体は汎用性がある技術なんで、「汎用性のあるデコレータの作成」と言う意味でも説得力がある。積分デコレータはデコる関数は数学的関数でありさえすれば基本的にその種は問わない、からだ。
最後にこの記事では恐らく一番役立ちそうな「引数付きデコレータ」を紹介しよう。
最初の方に「汎用デコレータ」を思いつくのは容易ではない、と言う話をした。何故なら汎用的な高階関数を思いつく事が難しいから、だ。
ヒントとしては、だったら「汎用的な高階関数」を思いつけないのなら、既にある「汎用的な高階関数」自体をデコレータの一部として採用しちまえばいい、って事だ。
そういう高階関数にいっつも言ってる最も汎用的な高階関数、functools.reduceがある。こいつの本質は実は繰り返し構文の抽象化だ。
よってこういうデコレータをでっち上げる事が可能だ。
from functools import reducedef irec(base = None):
def wrapper(func):
def self(iterable):
return reduce(
func, reversed(list(iterable)), [] if base == None else base
)
return self
return wrapper
irecはiterable recurserを意図している。このデコレータは@構文で定義された処理を使った繰り返しをする関数を自動生成する。
例えばこの通り、だ。
@irec(0)
def length(base, elm):
return 1 + base
@irec(0)
def my_sum(base, elm):
return base + elm
@irec(1)
def my_product(base, elm):
return base * elm
from decimal import Decimal
@irec(Decimal('-Infinity'))
def my_max(base, elm):
return base if base > elm else elm
@構文で2引数関数を定義する。第一引数ではbaseを指定し、第二引数ではイテラブルから取ってきた要素を指定し、その二者間での「計算」を定義するわけだ。
最初の例だと「イテラブルから取ってきた」要素は関係なく、「イテラブルから何かを取り出す」度にbaseに1を加算していく。結果、Pythonにはlenがあるが、同様にイテラブルの「長さ」を計測出来る。
二番目の例だと、baseに対しイテラブルから取ってきた「要素」(数値と仮定してるが)を粛々と加算していく。結果、Pythonのsumと同様の計算が可能だ。
三番目の例は、二番目とほぼ同じ構成だけど、積算を行っている。結果、与えられたイテラブルに含まれた数値を全部掛けた数を返す。
四番目の例は、与えられたイテラブルの中の数値から最大値を返す、Pythonのmaxと同様の効果をもたらす。
>>> length(range(5))
5
>>> my_sum(range(5))
10
>>> my_product(range(1, 5))
24
>>> my_max(range(5))
4
functools.reduceは書式が厳しいので、使い方が分からん、と萎縮する人が多いが、これなら「二引数関数を@構文で定義する」だけで、勝手に繰り返し付きの関数へと変換してくれるんで、もはや悩む必要もなくなるだろう。っつーかイテラブルに対して繰り返しを記述する必要が無くなったと言う事だ(笑)。
こんなんで汎用性あるの?って疑う人もいるだろうが、まぁ、ある。Python組み込み関数の再実装が可能だ、って辺りで疑う人もいるだろうが(しかし一方、「ビルトイン関数を再実装可能」な以上に「汎用性」の保証はないんだけど)、例えばこんな例はどうだろうか。
@irec()
def fib(base, elm):
return (0,) if base == [] else base + (1,) if len(base) == 1 else base + (base[-1] + base[-2],)
irecデコレータを使えばフィボナッチ数列でさえも実装可能だ。見た通り、繰り返しは特に定義せず、単に条件分岐を記述した関数を、自動的に繰り返しを含むフィボナッチ数列生成関数へと変換してくれる。
>>> fib(range(1))
(0,)
>>> fib(range(2))
(0, 1)
>>> fib(range(3))
(0, 1, 1)
>>> fib(range(10))
(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
注意事項は、この引数付きのirecデコレータを使う際、このフィボナッチ数列のように「引数が要らない」場合、()を付けてデコレータを指定し、@構文を書く必要がある、と言う事。
あとは、baseがNoneだった時、初期値は空リストになる、って事さえ分かれば、「フィボナッチ数列を処理する」記述自体は簡単に書けるだろう。
とまぁ、Pythonの「引数付きデコレータ」に関しては以上、かな。
あとは各人、色々と研究してみて欲しい。
※1: このSchemeに於けるユーティリティ作成の理論的な流れに付いては以前書いた事がある。
※2: 関数型言語や、Prologのような論理型言語では「真偽値を返す関数」を述語と表記する。
※3: これはRustでも同じだが、ここで龍虎氏が面白い事を書いている。
しかしプログラミング学習、最初からRustにしなくて良かった・・絶対にプログラミングが嫌いになってた自信あるわ〜・・「変数」なのにデフォルトでimmutableっておかし無いスか?なんで毎回mutつけんといかんのかな〜とか!
しかし、実の事を言うと、龍虎氏がRacketでプログラミングしてる際、set!なんて使ってなかった筈だ。つまり、事実上、変数はimmutableとして扱ってた筈、なんだ。
言い換えると、「mutを付ける」と言うペナルティ(事実上ペナルティだ・笑)込みでRustプログラミングをするのが間違いであり、結果、教える方が悪い、と言う衝撃的事実があるだけ、なんだ。
Rustは本来、非破壊的プログラミングをする、ってのが原則で、言い換えるとmutを付けまくった変数を扱うプログラミングはしちゃいけないってのが原則だ。それでもやっちゃう「教え手」がいる・・・そうだな、C言語脳だ(笑)。C言語脳はRustが構文的に「C言語に似てる」と言う理由だけ、でRustでもC言語的なプログラミングをしようとする・・・・・・。
なお、事実は、Rustはかなり関数型言語に近い。そしてその採用は平行計算に於いて、関数型言語が有用だ、と言う知見に依るから、であり、データがmutableで、平行計算でデータが安易に「書き換えられる」のを防ぐ為だ・・・計算Aも計算Bもあるデータを共用してる場合、例えば計算Aがデータを書き換えてしまったら計算Bの結果が狂ってしまう。それを避ける為には「データの書き換えを禁止するのをデフォとする」のが正しい。
しかし、C言語脳はC言語型プログラミングが習慣になりすぎて「捨てられない」。よってRustでRustらしからぬC言語なプログラミングを行って他人に紹介するわけだ。
そういう弊害の一つ、って考えていいだろう。