Pythonのデコレータで関数の型チェックなんかをやってみます.デコレータ構文を使うと関数定義のルーチンワーク的な部分を別の関数に切り離し,本質的に重要な部分を強調して記述できます
そもそもデコレータって何?という方は以下の説明をご覧ください
- http://www.kenjisato.jp/jpn/2011/08/02/python-decorator-expression/
- http://www.kenjisato.jp/jpn/2011/08/03/python-decorators-2/
とはいえ,僕にはあんまり高度なことはできませんので,とても簡単な例を通して雰囲気を理解することにとどめます.
Pythonは変数の型は動的に変化する高度な実装になってますので,関数の引数におかしな型の変数が入ってきてもそれを阻止する機構というのはありません.たとえば単純な階乗計算
def factorial(n):
if n = 0:
return 1
else:
return n * factorial(n-1)
を定義した上で,
>>> factorial("hoge")
とやると,『factorialは文字列型の引数はとれません』とはならず(そりゃそうだけど)に『文字列と整数の引き算はサポートされてない』となります.つまり,引き算やってみたけどダメでしたというエラーになります.また
>>> factorial(-1)
とか
>>> factorial(1.1)
とやると『リカージョンの階層が深すぎですよ』と怒られます.非負整数しか受け入れない関数として正しく定義するためには,
def factorial(n):
def _factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
if isinstance(n, int) and n >= 0:
return _factorial(n)
else:
raise ValueError, "Argument must be a positive integer"
『整数しかダメ』とか『正の数しかダメ』というのはよくある条件なので,関数を定義するたびにこのような条件を入れるのはあまり好ましくありません.コードが読みにくくなってしかたありません.というわけで,この型チェックと範囲チェックをデコレータを使ってやってみましょう
まずは型チェックから
前回・前々回の議論から以下のように書けるかと思います
def type_check(param):
def _type_check(func):
def wrapped(arg):
if isinstance(arg, param):
return func(arg)
else:
raise TypeError, "Argument must be " + param.__name__
wrapped.__name__ = func.__name__
wrapped.__doc__ = func.__doc__
wrapped.__dict__ = func.__dict__
return wrapped
return _type_check
@type_check(int)
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
実行すると期待通りの振る舞いをします.つまり,整数以外の引数を入れるとエラーを送出しますが,負の整数を入れるともともと起こっていたリカージョンの深度が大きすぎるというエラーで止まります.負数に対するコントロールはまだできていません.
(型チェックに関するより高度な手法についてはこちらのサイトが参考になります http://d.hatena.ne.jp/griefworker/20110328/python_inspect)
次は正数条件をつけます
def range_check(Min = None, Max = None):
def _range_check(func):
def wrapped(arg):
if Min == None and Max == None:
return func(arg)
elif Min == None:
if arg <= Max:
return func(arg)
else:
raise ValueError, "Argument must lie in (-Inf,{0}]".format(Max)
elif Max == None:
if arg >= Min:
return func(arg)
else:
raise ValueError, "Argument must lie in [{0},Inf)".format(Min)
else:
if Min <= arg <= Max:
return func(arg)
else:
raise ValueError, "Argument must lie in [{0},{1}]".format(Min, Max)
wrapped.__name__ = func.__name__
wrapped.__doc__ = func.__doc__
wrapped.__dict__ = func.__dict__
return wrapped
return _range_check
@range_check(Min = 0)
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
Min と Max をデコレータのパラメータとして指定しています.ちょっと長いのは上下限のパラメータが与えられた場合と与えられてない場合に対してそれぞれ処理を書き込んでいるためです.正数条件だけを実装しようとするならもう少し短くできますが,より一般的な形でやってみました.
この場合,正の整数を引数として factorial() に突っ込んだ場合はうまく動きます.負の整数を引数として入れると,望んだ通りのValueError が送出されます.正の小数をを入れると,再帰構文の終了条件 == 0 が満たされることがないので,負の数までさかのぼって factorial() を呼びだそうとします.このタイミングで正数条件が効いてくるので,結果ValueErrorが起こり,正ではない引数が渡されたと怒られます.
最後にくっつける
以上の2つをくっつけると上手く行くはずです.
@range_check(Min = 0)
@type_check(int)
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
デコレータを適用する順番はこの場合はどちらでも問題ありません.両方のチェックを通った引数だけが関数に渡されるので,そうでない場合はエラーメッセージの違いがあるかと思いますが,正しい引数に対してはなんの違いもないはずです.
最後のfactorial()の定義を見て分かるように,使い回しが可能なルーチン(ここでは型チェックと範囲チェック)はデコレータ構文で使えるように一度コーディングしておけば,本質的に重要な部分にフォーカスできます.さらに他の関数を定義する際にももちろん使えるので,コード全体をすっきりさせることができます
デコレータの本格的に実際的な問題に興味のある方は適当な文献を参照してください.たとえばこことか? あるいは,日本語の本ならこれとか:
アスキー・メディアワークス
売り上げランキング: 30189








