Pythonのススメ8

はじめに

Pythonでプログラムを書くにあたり、文法や言語仕様などの個人的なメモを記載する。
今回のネタは関数。


関数(基本)

関数定義時、「これは関数ですよ」と示すためにPythonではdefを使う。

def 関数名(引数):
    # 何かしらの処理

サンプルプログラムを以下に示す。

def myPrint(param):
    print(param)

myPrint("Hello, world!")
# 出力結果:Hello, world!

クラス内に関数を定義するときは、第一引数にselfを指定する必要がある。
※@staticmethodや@classmethodデコレータが修飾されている場合など、例外はある。
サンプルプログラムを以下に示す。

class MyClass:
    def __init__(self, param):
        self.param = param
    def myPrint(self):
        print(self.param)

myClass = MyClass("Hello, world!")
myClass.myPrint()
# 出力結果:Hello, world!


関数(オーバーロード

前記事(Pythonのススメ7 - Hello, world.)でも述べたとおり、Pythonでは関数のオーバーロードができない
例えば1.引数を受け取り、2.その引数の型と内容を表示する関数を定義するとき、オーバーロードができないので以下のような関数定義が考えられる。

def show_parameter(param):
    if type(param) == int:
        print("[引数はint型です.]")
    elif type(param) == float:
        print("[引数はfloat型です.]")
    elif type(param) == list:
        print("[引数はlist型です.]")
    elif type(param) == str:
        print("[引数はstr型です.]")
    else:
        print("[その他の型:" + str(type(param)) + "]")
    show_parameter_content(param)


def show_parameter_content(param):
    print(param)


show_parameter("AAA")
show_parameter([1, 2, 3])
show_parameter(123)
show_parameter(1.23)
show_parameter((1, 2, 3))

動作結果は以下の通り。

[引数はstr型です.]
AAA
[引数はlist型です.]
[1, 2, 3]
[引数はint型です.]
123
[引数はfloat型です.]
1.23
[その他の型:<class 'tuple'>]
(1, 2, 3)

意図したとおりに動いているのでこれで良いといえば良いが、いくつか課題点がある。

  • サンプルプログラムではパラメタの型に応じて条件分岐しているが、条件分岐が増えたとき、例えば、分岐させる条件としてtupleが増えたときは都度show_parameter関数に手を加える必要が出てくる。
  • 1関数当たりのステップ数が増えてしまいうる。上記サンプルプログラムではかなり単純な処理しかしていないので気にならないが、各条件ごとに処理を都度書いていると、1関数のステップ数が大きくなってしまう。1関数にあらゆる処理をまとめて書くのはソースコードの可読性の低下にもなりかねない。


@singledispatchデコレータ

じゃあどうすればいいのか。答えは、@singledispatchデコレータを使って処理をdispatchさせることである。
具体的には、以下のように記載する。
※ディスパッチは1つ目の引数の型で行われる。2つ目の引数の型でディスパッチとかはできないみたい。

@singledispatch
def 関数名(引数):
    # 処理

@関数名.register(型)
def 関数名A(引数):
    # 処理

@関数名.register(型)
def 関数名B(引数):
    # 処理

・・・

…おそらくサンプルプログラムを書いた方がイメージがつかみやすいと思うので、@singledispatchを使ったサンプルプログラムを以下に示す。これは、↑で書いたサンプルプログラムを、@singledispatchを使って書き換えたものである。

from functools import singledispatch


@singledispatch
def show_parameter(param):
    print("[その他の型:" + str(type(param)) + "]")
    show_parameter_content(param)


@show_parameter.register(int)
def _show_parameter(param):
    print("[引数はint型です.]")
    show_parameter_content(param)


@show_parameter.register(float)
def _show_parameter(param):
    print("[引数はfloat型です.]")
    show_parameter_content(param)


@show_parameter.register(list)
def _show_parameter(param):
    print("[引数はlist型です.]")
    show_parameter_content(param)


@show_parameter.register(str)
def _show_parameter(param):
    print("[引数はstr型です.]")
    show_parameter_content(param)


def show_parameter_content(param):
    print(param)


show_parameter("AAA")
show_parameter([1, 2, 3])
show_parameter(123)
show_parameter(1.23)
show_parameter((1, 2, 3))

実行結果は、@singledispatchを使う前と同じ。
このようにする利点は以下の通り。

  • 分岐対象が増えたとしても、「@関数名.register(型)」をデコレートして関数定義するだけでよい。例えばtuple型の場合の処理も追加したいな、となったときは以下のようにするだけでよい。
@show_parameter.register(tuple)
def _show_parameter(param):
    # 処理
  • 型によって処理内容が別の関数に定義されるので、@singledispatchを使う前のような、(全ての場合の処理を1つの関数にまとめて書くので)1関数のステップ数が膨大になりうる、という懸念を回避することができる。


終わりに

本投稿では、Pythonの関数について、サンプルプログラムを交えて、言語仕様とともに述べた。
関数を定義するときはdefを使う。
Pythonの言語使用上、そのままでは関数のオーバーロードはできないが、@singledispatchデコレータを使うことでオーバーロード(のような実装)を実現することができる。

…ただ、ここで思うことが一つ。「ディスパッチは1つ目の引数の型で行われる」のであれば、クラス内に定義された関数においては@singledispatchによるオーバーロード(のような実装)は実現できないということでは?クラス内に定義された関数の引数はselfなので。

クラス内に定義された関数でオーバーロード(のような実装)を実現しようと思ったらどうするんだろう…?


参考文献

10.2. functools — 高階関数と呼び出し可能オブジェクトの操作 — Python 3.6.5 ドキュメント
Python 3.4.0 の新機能 (3) - Single-dispatch generic functions - Qiita
Python の singledispatch で引数の型ごとに処理を分ける | CUBE SUGAR STORAGE
OOP in Python