見出し画像

Python 3: Deep Dive (Part 1 - Functional): シングルディスパッチ (セクション7-8/11)

  • Pythonでは関数のオーバーロードがサポートされていないため、型に応じた異なる処理を実現するために、`functools`モジュールの`singledispatch`デコレータを使用してシングルディスパッチ関数を作成できます。

  • `singledispatch`は引数の型に基づいて適切な関数を呼び出し、階層的なクラス継承に対応して最も適切な関数を選択するため、柔軟で効率的な汎用関数を実現します。

  • ただし、抽象基底クラス(ABC)を利用する際には無限再帰のリスクがあり、型の登録やハンドリングに注意が必要です。

Pythonの関数型プログラミング機能の旅を続けると、高度で強力な概念である「シングルディスパッチジェネリック関数」に到達する。Python 3: Deep Dive (パート1 - 関数型)のセクション7のレッスン115から117では、シングルディスパッチを使用してPythonで関数オーバーロードを実装する方法を探り、より柔軟で保守性の高いコードを実現する。


関数のオーバーロードの紹介

JavaやC++のような静的型付け言語では、関数のオーバーロードにより、複数の関数に同じ名前を付け、異なるパラメータを指定することができる。コンパイラは関数シグネチャに基づいて呼び出す関数を決定する。

例えば:

void doSomething(int number);
void doSomething(String text);

`doSomething(10)` を呼び出すと最初の関数が呼び出され、`doSomething(「Hello」)` を呼び出すと2番目の関数が呼び出される。


Pythonにおける課題

Pythonは動的型付け言語であり、変数には固定された型がなく、関数のシグネチャでパラメータの型を指定することはできない。この柔軟性は強力であるが、関数のオーバーロードを実装しようとする場合には課題となる。

Pythonでは、すべての関数は実行時に動的に解決され、関数シグネチャで型を指定しないと、インタプリタは同じ名前の関数を区別できない。


ディスパッチャー関数の作成

異なるデータタイプをHTMLにフォーマットする関数を作成すると仮定しよう。 異なるフォーマットが必要なものは以下の通りである。

  • 整数:10進数と16進数で表示する。

  • 浮動小数点数:小数点以下2桁に丸める。

  • 文字列:HTML 文字をエスケープし、改行を置換する。

  • リスト/タプル:箇条書きで表示する。

  • 辞書:キーと値のペアをリストで表示する。

まず、それぞれの型に対して個別の関数を書くことから始めよう。

from html import escape

def html_escape(arg):
	return escape(str(arg))

def html_int(a):
	return f「{a}(<i>{hex(a)}</i>)」

def html_real(a):
	return f"{round(a, 2):.2f}」

def html_str(s):
	return html_escape(s).replace('\n', '<br/>')

def html_list(l):
	items = [f"<li>{html_escape(item)}</li>" for item in l]
	return '<ul>' + '\n'.join(items) + '</ul>'

def html_dict(d):
	items = [f"<li>{html_escape(k)}={html_escape(v)}</li>" for k, v in d.items()]
	return '<ul>\n' + '\n'.join(items) + '\n</ul>'

手動によるシングルディスパッチの実装

デコレータの構築

htmlize 関数内で `if-elif` チェーンが長くなるのを避けるために、シングルディスパッチデコレーターを作成することができる。このデコレーターは、型をキーとする関数のレジストリを維持し、引数の型に基づいてどの関数を呼び出すかを決定する。

def singledispatch(fn):
	registry = {}
	registry[object] = fn # デフォルト関数

def register(type_):
	def inner(fn):
		registry[type_] = fn
		return fn
	return inner

def decorator(arg):
	fn = registry.get(type(arg), registry[object])
	return fn(arg)

decorator.register = register
	return decorator

関数の登録

このデコレータを使用して、特殊化された関数を登録することができる。

@singledispatch
def htmlize(arg):
	return escape(str(arg))

@htmlize.register(int)
def html_int(a):
	return f「{a}(<i>{hex(a)}</i>)」

@htmlize.register(float)
def html_real(a):
	return f"{round(a, 2):.2f}」

@htmlize.register(str)
def html_str(s):
	return escape(s).replace('\n', '<br/>')

@htmlize.register(list)
@htmlize.register(tuple)
def html_list(l):
	items = [f「<li>{htmlize(item)}</li>」 for item in l]
	return '<ul>' + '\n'.join(items) + '</ul>'

@htmlize.register(dict)
def html_dict(d):
	items = [f「<li>{htmlize(k)}={htmlize(v)}</li>」 for k, v in d.items()]
	return '<ul>\n' + '\n'.join(items) + '\n</ul>'

これで、異なる型を引数として `htmlize` を呼び出すと、適切な関数にディスパッチされる。

複数の型の処理

手動で実装したものは動作するが、次のような制限がある。

  • サブクラスを処理できない。

  • すべての型を事前に知っておく必要がある。

  • 抽象基底クラスを処理できない。


`functools.singledispatch` の使用

手動実装の限界を克服するために、Python は `functools.singledispatch` を提供している。

手動実装に対する利点

  • サブクラスの処理:最も近い型を自動的に選択する。

  • 抽象基底クラスのサポート:`numbers.Integral` や `collections.abc.Sequence` のような抽象型に対して関数を登録できる。

  • 組み込みで最適化:標準ライブラリの一部であり、堅牢で効率的である。

`@singledispatch` での登録

まず、ベースとなる関数をインポートしてデコレーションする。

from functools import singledispatch

@singledispatch
def htmlize(arg):
	return escape(str(arg))

次に、特殊化された関数を登録する。

from numbers import Integral
from collections.abc import Sequence

@htmlize.register(Integral)
def htmlize_int(a):
	return f「{a}(<i>{hex(a)}</i>)」

@htmlize.register(str)
def htmlize_str(s):
	return escape(s).replace('\n', '<br/>\n')

@htmlize.register(Sequence)
def htmlize_sequence(l):
	items = [f"<li>{htmlize(item)}</li>" for item in l]
	return '<ul>\n' + '\n'.join(items) + '\n</ul>'

これで、`htmlize` は整数、文字列、シーケンス(リストやタプルを含む)を正しく処理できるようになった。


実用的な例

HTML フォーマット関数

functools.singledispatch を使用すると、さまざまなデータタイプをエレガントに HTML にフォーマットすることができる。

print(htmlize(100))
# 出力: 100(<i>0x64</i>)

print(htmlize(["Item 1", 2, 3.1415]))
# 出力:
# <ul>
# <li>Item 1</li>
# <li>2(<i>0x2</i>)</li>
# <li>3.14</li>
# </ul>

抽象基底クラスの処理

抽象基底クラスに機能を登録することで、幅広い種類の処理が可能になる。

@htmlize.register(Sequence)
def htmlize_sequence(l):
# リスト、タプル、その他を処理する...


@htmlize.register(Mapping)
def htmlize_mapping(d):
# 辞書およびその他のマッピング型を処理する...

よくある落とし穴とその回避方法

文字列による無限再帰

Pythonの文字列はシーケンスであるため、関数をSequenceに登録すると、文字列を誤って捕捉してしまい、無限再帰を引き起こす可能性がある。

問題のあるコード:

@htmlize.register(Sequence)
def htmlize_sequence(l):
	items = [htmlize(item) for item in l]
	return '<ul>\n' + '\n'.join(items) + '\n</ul>'

解決策:

文字列用の特定の関数を登録して、シーケンスハンドラをオーバーライドする。

@htmlize.register(str)
def htmlize_str(s):
	return escape(s).replace('\n', '<br/>\n')

登録の順序が重要

ディスパッチャは最も特定の型を選択する。特定の型よりも後に、より一般的な型を登録すると、予期せぬ動作を引き起こす可能性がある。

特定の型が一般的な型の後に登録されるようにする。


まとめ

シングルディスパッチ汎用関数は、Pythonで関数のオーバーロードを実装するための強力なメカニズムを提供し、コードの柔軟性と保守性を向上させる。functools.singledispatchを活用することで、単一の引数の型に基づいて異なる動作をする関数を書くことができる。

主な要点:

  • Pythonにおける関数のオーバーロードはシングルディスパッチを使用することで実現できる。

  • 手動によるシングルディスパッチの実装は学習にはなるが、限界がある。

  • functools.singledispatchはサブクラスと抽象基底クラスを効率的に処理する。

  • 登録の順序と特異性は、よくある落とし穴を避けるために重要である。

  • 実用的な応用には、異種データのフォーマット、シリアライゼーション、処理などがある。

シングルディスパッチを習得することで、Pythonの関数型プログラミングの強力なツールが利用可能になり、よりクリーンで拡張性の高いコードを書くことができるようになる。


Pythonの高度な機能に関するさらなる詳細情報をお楽しみに!

「超本当にドラゴン」へ

この記事が気に入ったらサポートをしてみませんか?