@singledispatchmethodとは

きっかけ

コードレビュー中に見かけたので、調査

2, 3個の個人ブログやらを経由してやっと理解したので、ここでまとめておきたい

あと前提の知識が必要なやつだったので、一気に理解できると嬉しいよねって気持ち

ちなみに @singledispatchmethod を理解するにあたり、以下の順番で理解すると良さそう

  1. ジェネリック関数
  2. singledispatch
  3. singledispatchmethod ←今回学ぶやつ

前置き長いですが、順番に理解する方が意外と近道かもです 🐥

ジェネリック関数

  • 「第一引数の型に応じて、異なる処理を行う関数」

ざっくりイメージとしては

def function(obj):
  if type(obj) == int:
    return print(f'int型が渡されました,obj={obj}')
  
  if type(obj) == str:
    return print(f'string型が渡されました,obj={obj}')

# int型渡す
function(1)

# string型を渡す
function('moji')

===========結果==================
int型が渡されました,obj=1
string型が渡されました,obj=moji

こんな感じで引数の型によって function 関数の挙動が変わる

こういうやつをジェネリック関数 というらしい

もっと詳しくの方は、以下を参考にしてください。

https://retroid2016.com/2018/08/09/【pyhton】ジェネリック関数【備忘録】/

singledispatch

  • Pythonから提供されているライブラリである「functools」の一部

公式ライブラリは以下ですが、まぁ・・・わからなかった

https://docs.python.org/ja/3/library/functools.html#functools.singledispatch

  • ジェネリック関数を定義するためのデコレータ
  • つまり、ジェネリック関数を作りたい!ってなったら、関数の先頭につけるデコレータ
  • クラスの中にある関数(正しくはメソッド と言いますが)ではない、関数用のデコレータ

ジェネリック関数の説明で書いたソースコードを、singledispatch で書き換えてみる

from functools import singledispatch

@singledispatch
def function(obj): <- 元となる関数名
  return print(f'{type(obj)}型が渡されました, obj={obj}')

# int型がきた時の関数の挙動を宣言
# ちなみに関数名が 「 _ 」なのは、何でもいいよって意図らしいです
@function.register  <- 元となる関数名.register で定義する
def _(obj: int):
    return print(f'int型が渡されました,obj={obj}')

# string型が来たときの関数の挙動を宣言
@function.register
def _(obj: str):
    return print(f'string型が渡されました,obj={obj}')

# int型渡す
function(1)

# string型を渡す
function('moji')

# list型を渡す
function([1, 2, 3])

===========結果==================
int型が渡されました,obj=1
string型が渡されました,obj=moji
<class 'list'>型が渡されました, obj=[1, 2, 3]

ちゃんと型によって挙動が変わった・・・!

すごい便利なデコレータですね 👀

では、やっとですが、本来知りたいやついきましょう。

ちなみに、このsingledispatch がバッチリ理解できた人は、もう singledispatchmethod も理解したに等しいですので、ご安心を。。。

singledispatchmethod

  • 同じく、Pythonから提供されているライブラリである「functools」の一部

まぁ、同じく公式ドキュメント難しい

https://docs.python.org/ja/3/library/functools.html#functools.singledispatchmethod

  • singledispatch は関数用でしたが、singledispatchmethod はメソッド用
  • つまり、クラスの中とかで定義されたメソッドをジェネリック関数にしたいとなったら、こっちを使いましょう ということ
  • そして使い方は、singledispatch と一緒です 🙌

singledispatch で書いたソースコードをクラスの中のメソッドにして、singledispatchmethod で書き直してみましょう

from functools import singledispatchmethod

class Func:

  @singledispatchmethod
  def function(self, obj):
    return print(f'{type(obj)}型が渡されました, obj={obj}')

  # int型がきた時の関数の挙動を宣言
  @function.register
  def return_int(self, obj: int): <- 今回はメソッド名をつけてみた
      return print(f'int型が渡されました,obj={obj}')

  @function.register
  def return_str(self, obj: str):
      return print(f'string型が渡されました,obj={obj}')

# Funcクラスを作成
func = Func()

# int型渡す
func.function(1) <- 元の関数名で呼んであげるで良さそう

# string型を渡す
func.function('moji')

# list型を渡す
func.function([1, 2, 3])

===========結果==================
int型が渡されました,obj=1
string型が渡されました,obj=moji
<class 'list'>型が渡されました, obj=[1, 2, 3]

ちなみに他のデコレータと一緒に使いたいってなった時は、以下のようにsingledispatchmethod を一番外にして書いてあげると良いらしい

今回は staticmethod を追加してあげる

=> については以下を見るとわかりやすいかも

https://www.lifewithpython.com/2014/02/python-difference-between-staticmethod-and-classmethod.html

from functools import singledispatchmethod

class Func:
  @singledispatchmethod
  @staticmethod
  def function(obj):
    return print(f'{type(obj)}型が渡されました, obj={obj}')

  # int型がきた時の関数の挙動を宣言
  @function.register
  def return_int(obj: int):
      return print(f'int型が渡されました,obj={obj}')

  @function.register
  def return_str(obj: str):
      return print(f'string型が渡されました,obj={obj}')

# int型渡す
Func.function(1)

# string型を渡す
Func.function('moji')

# list型を渡す
Func.function([1, 2, 3])

ちなみに、今回調べていて、公式ドキュメントを参考に書いてみたら、動かない現象を発見しましたので、一応メモ

以下のコードを動かすと

class Negator:
    @singledispatchmethod
    @classmethod
    def neg(cls, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    @classmethod
    def _(cls, arg: int):
        return -arg

    @neg.register
    @classmethod
    def _(cls, arg: bool):
        return not arg

以下のエラーが出ます

TypeError: Invalid first argument to `register()`: <classmethod object at 0x1043063d0>. Use either `@register(some_class)` or plain `@register` on an annotated function.

↑のコードは、 Python3.8.1 と3.8.3 でしか動作しないコードなのでご注意を・・・

どう修正すればいいのかまでは、調べてないですが・・・

1年前はまだ解決してなさそうですが、今はどうなんでしょう・・・ 🤔

参考:https://stackoverflow.com/questions/62696796/singledispatchmethod-and-class-method-decorators-in-python-3-8

まとめ

こちらのデコレータを使うと、何がいいかってif文が乱立するソースコードではなくなり、可読性があがりそうって感じでしょうかね 👐

今回の調査で参考にしたサイトたち

https://smooth-pudding.hatenablog.com/entry/2021/03/27/145218#class-functoolssingledispatchmethodfunc-ver38

https://marusankakusikaku.jp/python/standard-library/functools/#singledispatch-ジェネリック関数の定義