Python 3.0 Hacks

第7回 関数アノテーションでスマートにプラスアルファの実現

この記事を読むのに必要な時間:およそ 3 分

関数アノテーションとは

関数アノテーションとは関数の引数と戻り値に付加情報をつける機能です。PEP 3107 -- Function Annotationsで定義されていて,Python 2.6へbackportされていないので,利用するにはPython 3.0以降が必要になります。

まずは,インタラクティブシェルで関数アノテーションの例を見てみましょう。

>>> def foo(a: "a argument", b: int) -> ["return", "value"]:
...     return a+b
...
>>> help(foo)

Help on function foo in module __main__:

foo(a: 'a argument', b: int) -> ['return', 'value']

>>> foo.__annotations__
{'a': 'a argument', 'b': <class 'int'>, 'return': ['return', 'value']}
>>> foo(3,5)
8

このように,引数のアノテーションは引数の後に `: expression` の形で,戻り値のアノテーションは引数リストの後に `-> expression` の形で定義します。

アノテーションの内容は関数オブジェクトの `__annotations__` というプロパティに保存されます。そして,関数の実行にはまったく影響しません。

今回は,この関数アノテーション(以降アノテーションと呼ぶ)の実用例をいくつか挙げていきます。

アノテーションでドキュメントを書く

Pythonでは一般的に docstring と呼ばれる方法で関数のドキュメントを書きます。引数や戻り値に関する説明も docstring の中に記述していました。たとえば以下のような感じです。

def load_config(conf_file):
    """設定ファイルを読み込む.
    
    file は設定ファイルのパスを示す str オブジェクト。
    設定値を格納した dict オブジェクトを返す。"""

アノテーションは help() や pydoc で表示されるので,Python3.0 からは引数や戻り値に関するドキュメントをアノテーションとして簡潔に記述することもできます。

def load_config(conf_file: "設定ファイルのパス (str)") -> "設定値 (dict)":
    """設定ファイルを読み込む."""

...
>>> help(load_config)

Help on function load_config in module __main__:

load_config(conf_file: '設定ファイルのパス (str)') -> '設定値 (dict)'
設定ファイルを読み込む.

anntoolsで自動型変換

アノテーションを活用しているライブラリとして,anntoolsを紹介します。

anntools は関数の引数や戻り値が想定している型や値になっているかどうかをチェックしたり,期待している以外の型のオブジェクトが渡されたときに自動で型変換したりしてくれたりします。

Python 2.6以前にも対応しているので,関数アノテーションのある・なしでどう変わるか見ていきましょう。BlogなどのWebアプリケーションで,記事を編集するリクエストを扱う関数を想定してみます。

def blog_edit(post_id):
    post_id = int(post_id)
    # ... post_id を元に記事を取得し編集画面を表示する

リクエストのURLから関数へマッピングするツール(URLマッパー)が自動で型変換してくれない場合は,このように関数内で型チェックや型変換が必要になります。

これを,Python 2.6とanntoolsで書いてみましょう。

>>> from anntools.conversion import *
>>> @convert(post_id = AsInt)
... def blog_edit(post_id):
...     print type(post_id)
... 
>>> blog_edit("32")
<type 'int'>
>>> blog_edit("foo-bar")
#tracebackは省略
ConversionError: Error converting argument 'post_id' of function 'blog_edit' by AsInt converter: post_id = 'foo-bar'

確かにうまく動くのですが,元のコードよりも長くなってしまっています。引数の数が増えてくるとanntoolsを使った方が短くなるかもしれませんが,デコレータに対していちいち引数の名前を指定してあげないといけないのが面倒で,あまり便利な気がしません。

今度は Python 3.0 以降で関数アノテーションを使ってみます。

>>> from anntools.conversion import *
>>> @convert
... def blog_edit(post_id: AsInt):
...     print(type(post_id))
... 
>>> blog_edit(3)
<class 'int'>
>>> blog_edit("32")
<class 'int'>
>>> blog_edit("thirty two")
anntools.conversion.ConversionError: Error converting argument 'post_id' of function 'blog_edit' by AsInt converter: post_id = 'thirty two'

アノテーションがあると型情報を関数に付けることができるので,デコレータに引数を与える必要がありません。引数を二度タイプする手間が省けてすっきりしますし,モジュールやクラス内の関数に対して,自動でデコレータを適用する仕組みも簡単に作れます。

anntoolsの型変換以外の機能として,引数のバリデーションをするサンプルも挙げておきます。

>>> from anntools.validation import *
>>> @validate
... def foo(n:Int(min=0, max=10)):
...    print(n)
... 
>>> foo(3)
3
>>> foo(-1)
anntools.validation.ValidationError: Error checking argument 'n' of function 'foo' with the Int validator: n = -1

範囲を指定できるなら,境界値テストを自動生成するテストツールなんてものも考えられますね。

著者プロフィール

稲田直哉(いなだ なおき)

KLab株式会社 Kラボラトリー所属。

数年前,久しぶりにPerlを使おうと思ったらPerlのコードを全く書けなくなっていた。自分の記憶力のなさに絶望し,Pythonを始める。IPythonを知って,Python無しには生活できなくなる。

2008年,Pythonドキュメント翻訳プロジェクトに参加し,不定期に翻訳中。一緒に翻訳してくれる方募集中。

コメント

コメントの記入