Make組ブログ

Python、Webサービスや製品開発、ライブラリー開発についてhirokikyが書きます

Python3.4のSingle-dispatchで遊んでみた - Python Advent Calendar 2013

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 から入る singleddispatch で遊んでみました。

single-dispatchって何?

singleddispatch は 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リポジトリ にあげてるのでそちらを参照してください。

実用例を考えてみた

さてこの singleddispatch 、たしかに面白いですが何に使えるでしょうか。 一つ考えてみたのは 「任意の型で返り値を返す関数たちについて、返り値を共通の型で包む」 というものです。ちょっと自分で言っててもよく分からないので、例を考えます。

チャットするロボットを考えます。人間とロボットは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デコレーターを取り外し可能にするなどして 対応するのが良いかもしれません。

まとめ

  • singleddispatch 面白い
  • 任意の型 => 共通の型 への変換などに使えそう

小さいながらも面白い機能で、とくにフレームワークやライブラリを提供するときに 使えそうな印象です。

遊びで書いたコードはここにおいていますので、より詳しくは読んでみてください:

以上です。2日目は露木さん(@everes)にお願いしたいと思います。