関数オーバーロード
関数のオーバーロードは、同じ名前で実装が異なる複数の関数を定義できる機能です。 C/C++などではオーバーロードされた関数が呼び出されると、最初に関数呼び出しに渡された引数/パラメーターを評価し、これによって判断して対応する実装を呼び出します。
Pythonの言語仕様では関数オーバーロードの機能はありませんが、実装する方法はあります。
ジェネリック関数を使う方法
functoolsモジュールの singledispatch デコレーターを使うと関数オーバーロードを実装することができます。
@singledispatchデコレーターは、関数をジェネリック関数に変換します。ジェネリック関数は、第一引数の型に応じて、同じような処理を異なった方法で行う関数のことです。デコレートされた関数は、どの型にも適合しなかったときのデフォルトの実装として機能します。
オーバーロードされた実装を関数に追加するには、@ジェネリック関数名.register() を使用します。
code: python
In 2: # %load sample_singledispatch.py ...: from functools import singledispatch
...:
...: @singledispatch
...: def myfunc(s):
...: print(s)
...:
...: @myfunc.register(int)
...: def squre(s):
...: print(s ** 2)
...:
...: @myfunc.register(list)
...: def joinword(s):
...: m=''
...: for e in s:
...: m += e
...: print(m)
...:
...: myfunc('OsakaPython')
...: myfunc(10)
...:
OsakaPython
100
OsakaPython
この例では、関数 myfunc() が @singledispatchでデコレートされてジェネリック関数となります。関数 squre() を @myfunc.register(int) で int型に登録します。
同様に、関数 joinword()を@myfunc.register(list)でlist型に登録します。
呼び出すときもジェネリック関数 myfunc() として呼び出し、第1引数の型により、squre() と joinword() に振り分けられ、どれにも適合しない型のときは、myfunc() の実装がそのまま動作します。
クラスで実装する方法 - Arpit Bhayani氏のアイデア
Functionクラス
関数をラップして、オーバーライドされた __call__メソッドで呼び出し可能にするFunction というクラスを作成します。
メソッドオーバーライドされた__call__メソッドはラップした関数を呼び出して処理された値を返します。 つまり、Functionクラスのインスタンスオブジェクトは関数と同じように呼び出し可能になり、ラップされた関数とまったく同じ動作となります。
key()メソッドは、ラップした関数をコード全体で一意にするためのタプルを返すようにします。このタプルには、引数の数も含まれています。
code: IPython
...: from inspect import getfullargspec
...:
...: class Function(object):
...: def __init__(self, fn):
...: self.function = fn
...:
...: def __call__(self, *args, **kwargs):
...: return self.function(*args, **kwargs)
...:
...: def key(self, args=None):
...: if args is None:
...: args = getfullargspec(self.function).args
...: return tuple([
...: self.function.__module__,
...: self.function.__class__,
...: self.function.__name__,
...: len(args or []),
...: ])
...:
...: return(n**2)
...:
In 4: func = Function(squre) In 5: func.key() Out5: ('__main__', function, 'squre', 1) ここで使用されている inspect.getfullargspec は与えた関数の引数やアノテーションの情報を抽出することができるものです。
code: Ipython
In 1: from inspect import getfullargspec ...: return n**2
...:
In 3: getfullargspec(squre) Out3: FullArgSpec(args='n', varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={}) In 4: getfullargspec(squre).args Out4: 'n' Namespaceクラス
定義したときに収集したすべての関数を仮想名前空間に格納するNamespaceクラスを作成します。
register()メソッドは、Functionクラスのkey()メソッドで取得したキーを使って辞書(function_map)に関数を保持します。これで、同じ名前で引数が異なるような関数でも区別しやすくなります。
code: IPython
...: class Namespace(object):
...: __instance = None
...:
...: def __init__(self):
...: if self.__instance is None:
...: self.function_map = dict()
...: Namespace.__instance = self
...: else:
...: raise Exception("Cannot instantiate Namespace again")
...:
...: @staticmethod
...: def get_instance():
...: if Namespace.__instance is None:
...: Namespace()
...: return Namespace.__instance
...:
...: def register(self, fn):
...: func = Function(fn)
...: return func
...:
In 13: namespace = Namespace.get_instance() In 14: func = namespace.register(squre) In 15: func(4) Out15: 16 register() が返すのはFunctionクラスのインスタンスオブジェクトで、これは登録した関数となります。
デコレータ
Pythonは、関数をラップするデコレータを使って、関数定義を変更せずに新しい機能を追加できることができます。
関数を登録する機能を持つ Namespaceクラスを定義したので、
これを使ってラップされた関数funcを引数として受け入れ、代わりに呼び出される別の関数を返す overload() を作成します。
code: IPython
...: def overload(func):
...: return Namespace.get_instance().register(func)
...:
このoverload()が返すオブジェクトは、前述のNamespaceクラスの例と同様に、呼び出し可能で、登録した関数と同じ動作になります。
引数の数から関数を特定する
Functionクラスのkey()メソッドが返す情報には引数の数も含まれています。
これをつかって関数を特定するような get()メソッドを Namespaceクラスに追加します。
code: python
def get(self, fn, *args):
func = Function(fn)
return self.function_map.get(func.key(args=args))
関数を呼び出す
Functionクラスの __call__メソッドは、デコレーターで修飾された関数が呼び出されるたびに呼び出されます。 この関数を使用して、Namespaceクラスのget()メソッドで適切な関数を取得して、デコレートした関数を呼び出すように修正します。
code: Python
def __call__(self, *args, **kwargs):
func = Namespace.get_instance().get(self.function, *args)
if not func:
raise Exception("No matching function found.")
return func(*args, **kwargs)
コードを整理すると次のようになります。
code: Python
from inspect import getfullargspec
class Function(object):
def __init__(self, func):
self.function = func
def __call__(self, *args, **kwargs):
func = Namespace.get_instance().get(self.function, *args)
if not func:
raise Exception("no matching function found.")
return func(*args, **kwargs)
def key(self, args=None):
if args is None:
args = getfullargspec(self.function).args
return tuple([
self.function.__module__,
self.function.__class__,
self.function.__name__,
len(args or []),
])
class Namespace(object):
__instance = None
def __init__(self):
if self.__instance is None:
self.function_map = dict()
Namespace.__instance = self
else:
raise Exception("Cannot instantiate Namespace again.")
@staticmethod
def get_instance():
if Namespace.__instance is None:
Namespace()
return Namespace.__instance
def register(self, fn):
func = Function(fn)
return func
def get(self, fn, *args):
func = Function(fn)
return self.function_map.get(func.key(args=args))
def overload(fn):
return Namespace.get_instance().register(fn)
code: IPython
In 1: %run overload.py In 2: %load sample_overload.py ...: @overload
...: def area(length, breadth):
...: return length * breadth
...:
...: @overload
...: def area(radius):
...: import math
...: return math.pi * radius ** 2
...:
...: @overload
...: def area(length, breadth, height):
...: return 2 * (length * breadth + breadth * height + height * length)
...:
...: @overload
...: def volume(length, breadth, height):
...: return length * breadth * height
...:
...: @overload
...: def area(length, breadth, height):
...: return length + breadth + height
...:
...: @overload
...: def area():
...: return 0
...:
In 5: area(2) Out5: 12.566370614359172 In 7: area(2,4,3) Out7: 9 In 8: volume(2,4,3) Out8: 24 問題点
この方法ではクラスのメソッドをオーバーロードすることができません。
また、同じ関数名で同じ数の引数で、受け取る型が違うような場合はうまく動作しません。
code: Python
...: def area(a: str, b: str):
...: return a+b
...:
...:
In 12: area('Python', 'Osaka') Out12: 'PythonOsaka' In 13: area(2,4) Out13: 6 文字列を2つ引数として受けとるarea()を定義しています。一見うまくいっているように見えますが、はじめに定義した area(length, breadth)の定義を上書きしてしまいます。
この方法の良くない点は、デコレータするタイミングでFunctionクラスの__init__()メソッドは呼び出されますが、__call__()は実際に関数が実行されて呼び出される点です。
つまりFunctionクラスのインスタンスオブジェクトが最後にデコレータした状態になったままになる点です。つまり、__call__()が呼ばれるときに使用するself.functionが実際にオーバーロードする関数とならないことが問題となります。
それでも、問題なく動作している理由は、Functionクラスのkey()メソッドが返すタプルの引数の数が辞書検索のキーとしてうまく機能しているからです。
もう少し説明しましょう。key()は次の4つの情報を持つタプルを返します。
関数のモジュール
関数が属するクラス
関数の名前
関数が受け入れる引数の数
__init__() が呼ばれて最後の関数をオーバーロードとして登録したとき、self.function の値は、__call__() を呼び出すことになる関数とは違うものです。
それでもkey()が返す値は引数の数を除いては幸いにして同じ値になります。
結果として、引数の数が登録している辞書function_map の検索キーとして機能して、本来の関数の情報を返すことができているわけです。
受け取る型を識別する
同じ関数名で、同じ数の引数をとり、受け取る型が違うようなときでもうまく関数オーバーロードできるようにするためにはどうすれば’よいでしょうか?
デコレータで修飾した関数が実行されるタイミングで呼び出される__call__()メソッドは、実際に与えられた引数の情報しか知ることができません。
引数の数と、引数の型、および引数の型の並び順から実際に実行するべき関数を見つける必要があります。
inspec.getfullargspec() では関数定義にある変数の変数名(文字列)しか受け取れません。数は知ることができても型は関数定義でアノテーションされている必要があります。
overload モジュールを使う方法
拡張モジュール overload を使うと関数オーバーロードを簡単に行うことができます。
使い方はベースになる関数を @overloadでデコレートして、オーバロードする関数を、
@ベース関数名.add でデコレートするだけです。
code: IPython
In 3: # %load sample_overload.py ...: from overload import overload
...:
...: @overload
...: def area(length:int, breadth:int):
...: return length * breadth
...:
...: @area.add
...: def area(radius:int):
...: import math
...: return math.pi * radius ** 2
...:
...: @area.add
...: def area(length:int, breadth:int, height:int):
...: return 2 * (length * breadth + breadth * height + height * length)
...:
...: @area.add
...: def area(length:float, breadth:float, height:float):
...: return length + breadth + height
...:
...: @area.add
...: def area(a: str, b: str):
...: return a+b
...:
In 5: area('Python', 'Osaka') Out5: 'PythonOsaka' In 6: area(1.0, 2.0, 3.0) Out6: 6.0 In 7: area(1, 2, 3) Out7: 22 ベースの関数でアノテーションされていないと引数の数だけで区別するようになるので、
オーバーロードする関数と引数の数が同じだとうまく動作しません。
code: Ipython
In 2: # %load test_overload3 ...: from overload import overload
...:
...: @overload
...: def func(a):
...: return 'with a'
...:
...: @func.add
...: def func(a, b):
...: return 'with a and b'
...:
...: @func.add
...: def func(c:float):
...: return 'float'
...:
...: @func.add
...: def func(c:str):
...: return 'str'
...:
...:
In 3: func(1) Out3: 'with a' In 4: func(1,2) Out4: 'with a and b' In 5: func('1') Out5: 'with a' ベースの関数とオーバーロードする関数の引数の数と引数のタイプが同じだと区別することができません。
code: IPython
In 2: # %load test_overload2 ...: from overload import overload
...:
...: @overload
...: def func(a:int):
...: return 'with a'
...:
...: @func.add
...: def func(a, b):
...: return 'with a and b'
...:
...: @func.add
...: def func(c:int):
...: return 'int'
...:
...: @func.add
...: def func(c:str):
...: return 'str'
...:
...:
In 3: func(1) Out3: 'with a' In 4: func(2) Out4: 'with a' 参考: