Python 3.0 Hacks

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

関数アノテーションとは

関数アノテーションとは関数の引数と戻り値に付加情報をつける機能です。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

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

アノテーションでoverloading

今度はアノテーションを使ってoverloading(多重定義)を実現してみましょう。

overloadingとは静的型付け言語では一般的な機能で、名前は同じだけど引数の型が異なる関数を別々に定義しておき、関数を呼び出すときには引数の型を元に、どの関数を使うかを決定するものです。

たとえばJavaのPrintStreamクラスでは println()メソッドをオーバーロードしていて、次のように使うことができます。

public class Test {
    static public void main(String[] args) {
        // System.out は PrintStream型
        System.out.println(3); // println(int) が呼ばれる. => "3"
        System.out.println(2 == 3); // println(boolean) が呼ばれる. => "false"
        System.out.println("foobar"); // println(String) が呼ばれる. => "foobar"
    }
}

もちろんPythonのprint関数(Python 3以前はprint文)も、いろいろな型のオブジェクトをうまく扱っているのですが、自分で渡された型に応じて動作を変える関数を作ろうとすると、自分で型チェックを行い分岐しなければなりませんでした。

from __future__ import print_function

def myprint(obj):
    if isinstance(obj, int):
        myprint_int(obj)
    elif isinstance(obj, str):
        myprint_str(obj)
    elif ...

def myprint_int(obj):
    print("<int>", obj, "</int>", sep="")

def myprint_str(obj):
    print("<str>", obj, "</int>", sep="")

...

このようにisinstance()の列を書いていると、overloadingのある静的片付け言語の方が楽かも……なんて思うこともあります。この手間を省く動的overloadingをGuido氏が実装したものが、PythonのSubversionリポジトリの中にあります

この中のprettyprinter.pyというサンプルで、overloadしている場所はこうなっています。

@overloaded
def pprint(self, obj):
    self.write(repr(obj))

@pprint.register(object, list)
def pprint_list(self, obj):
    if not obj:
        # ...

先ほどのPython 2.6+anntoolsと同じで、デコレータに型を指定しています。これをアノテーションを使うように書き換えてみました。

@overloaded
def pprint(self, obj):
    self.write(repr(obj))

@pprint.register
def pprint_list(self, obj: list):
    if not obj:
        # ...

アノテーションで型指定していない引数は自動でobject型として扱うようにしたので、selfの型指定が不要になりました。こちらの方が引数の順番を変えるのも楽そうです。

おわりに

いくつかのサンプルを挙げてみたのですが、デコレータだけでは記述が面倒だった関数の引数や戻り値に対する情報の紐づけが、アノテーションを使うことでスマートにできるようになるのを実感できたでしょうか?

Python 2.6ではアノテーションが使えないこともあり、アノテーションを使ったツールやフレームワークはまだあまりないのですが、Python 3.xが主流になるころには、いろいろなフレームワークでアノテーションを使ったスマートな機能が実現されていることでしょう。

おすすめ記事

記事・ニュース一覧