Python3.4のSingle-dispatchで遊んでみた - Python Advent Calendar 2013
Python Advent Calendar 2013 の 1 日目を担当します、 @hirokiky です。 昨年 に引き続きPythonAdventCalendarを主催しています。
ちなみに今年は現時点であと 4 人参加者が足りません!ぜひご参加ください
さて、今年の Python Advent Calendar のテーマは ‘Not Web’ ということなので、 Web に限らない話を書きます。
つい最近の11月24日に Python 3.4 の Beta 1 がリリースされました。 Python 3.4 b1 には無事 ensure-pip も入り、いよいよ Python 3 感が増してきました。 そんなところですが、ここで Python 3.4 から入る single-dispatch で遊んでみました。
single-dispatchって何?
single-dispatch は functools の一つで、第一引数の型に応じて処理を変更する generic function を作るものです。
単純な例で試してみます:
from functools import singledispatch
@singledispatch
def fun(arg):
return 'default'
## Registering behaviors to correspond to each types
@fun.register(int)
def fun_int(arg):
return 'int'
@fun.register(list)
def fun_list(arg):
return 'list'
assert fun(3) == 'int'
assert fun([]) == 'list'
assert fun('str') == 'default' # str type is not registered.
assert fun_int('dummy') == 'int'
assert fun_list('dummy') == 'list'
assert fun(object()) == 'default' # Using 'instance of object' to test the default behavior.
ポイント:
- singledispatch によって generic function を定義
- .registerによって、型と対応する処理を登録
- 通常の関数のように generic function を呼び出す
アンチパターン
さきほどあげた例では少し簡単すぎるので、利点が伝わりにくいかもしれません。 なのでここで、アンチパターンをあげておきます:
def fun(arg):
if isinstance(arg, int):
return 'int'
elif isinstance(arg, list):
return 'list'
else:
return 'default'
assert fun(1) == 'int'
assert fun([]) == 'list'
assert fun('str') == 'default'
ダメなポイント:
- 新しい型と対応する処理を後から追加できない
- 各型に応じた処理を取り出せない (先の例でいう ‘fun_int’ などを直接テストできない)
- 見難い
などですね。
他にもいくつか挙動を試していますが、長くなるので気になる人は Githubのリポジトリ にあげてるのでそちらを参照してください。
実用例を考えてみた
さてこの single-dispatch 、たしかに面白いですが何に使えるでしょうか。 一つ考えてみたのは 「任意の型で返り値を返す関数たちについて、返り値を共通の型で包む」 というものです。ちょっと自分で言っててもよく分からないので、例を考えます。
チャットするロボットを考えます。人間とロボットはMessageというオブジェクトでやりとりするとしましょう。 ロボットは単なるcallableで、Messageオブジェクトを受け取りMessageオブジェクトを返します。 ただ実装上、このロボットが返す値をいちいちMessageオブジェクトにしてやるのは面倒なので、 ロボットから返す値をMessageオブジェクトに変換する処理を挟んでやります。 この「何らかの型」 => 「Messageオブジェクト」の変換処理をgeneric functionとして持つわけですね。「共通の型」というのがこのMessageオブジェクトです。
さてまずはロボットの実装に必要な、ライブラリとしての処理を実装します。
- Message: ロボットとのやりとりに使うオブジェクトのクラス
- generate_message: 各型 => Messageに変換するgeneric function
- as_robot: 関数をロボットとして定義するデコレーター
class Message(object):
""" Messages to communicate each robots.
"""
def __init__(self, body, **metadata):
self.body = body
self.metadata = metadata
def __str__(self):
return '''\
{self.body}
* metadata: {self.metadata}
'''.format(self=self)
@singledispatch
def generate_message(arg):
""" Creating Message object for each types.
"""
raise TypeError('Unexpected type')
@generate_message.register(str)
def str_to_message(arg):
return Message(arg)
@generate_message.register(dict)
def dict_to_message(arg):
body = arg.pop('body')
return Message(body, **arg)
def as_robot(func):
def wrapped(*args, **kwargs):
ret = func(*args, **kwargs)
return generate_message(ret)
return wrapped
さてこれでロボットを実装する準備ができました。 ここまでをライブラリ、フレームワーク側から提供されるべきものと想定しています。 以下はそれを利用した、ユーザー側が書くべき処理の例です:
@as_robot
def antique(message):
return "Good morning, Master Ren."
@as_robot
def neomodel(message):
return {'body': "I'm here.",
'enjoyment': 1}
if __name__ == '__main__':
print('antique::', end=' ')
print(antique('dummy message'))
print('neomodel::', end=' ')
print(neomodel('dummy message'))
実装できました。
antique関数では文字列を直接返し、neomodel関数では辞書を返しています。 各関数はas_robotというデコレーターで包まれているので、戻り値がMessageオブジェクト で共通になります。
実行してやるとこんな答えが返ります:
antique:: Good morning, Master Ren.
* metadata: {}
neomodel:: I'm here.
* metadata: {'enjoyment': 1}
まぁこんなかんじで、任意の型 => 共通の型への変換処理を作るのにも使えるのではないか という例でした。 もちろんロボット関数がMessageオブジェクトを返した場合や、後から変換処理を追加する ことも考えられます。これもアップロードしてあるファイルから見てみてください。
ただこの例の場合as_robotデコレーターを外せないので、純粋な関数としての テストが難しいです。そこはas_robotデコレーターを取り外し可能にするなどして 対応するのが良いかもしれません。
まとめ
- single-dispatch 面白い
- 任意の型 => 共通の型 への変換などに使えそう
小さいながらも面白い機能で、とくにフレームワークやライブラリを提供するときに 使えそうな印象です。
遊びで書いたコードはここにおいていますので、より詳しくは読んでみてください:
以上です。2日目は露木さん(@everes)にお願いしたいと思います。