魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

methodcaller とか __call__ とかを見る

はじめに

最近は読んだ本、しかも技術書以外の小説を読んだ感想記事ばかり書いていて、エンジニアブログ感がなくなってしまったので無理矢理ネタを見つけて書きます。

operator モジュールの methodcaller 関数のソースコードを読む

まずは使い方を知る

Python 標準モジュールに operator モジュール があります。 簡単にいえば operator (演算子)を関数の形で表すモジュールです。

この辺りを使いこなせば関数型プログラミングのようなことができます。

単純なところでいえば a + badd(a, b) と書けたり、a - bsub(a, b) と書けたりします。 このあたりは ソースコード を読むとまじでそのままです。

これだけだとあまり面白くないですが、オブジェクト指向の書き方を関数型のように書くことができるようになっているのが面白いところで、その代表的なものとして methodcaller というメソッドがあります。

関数って言ってるのにメソッドなのかよと思われるかもしれませんが、これは呼び出し可能オブジェクトを使うことで実質的にオブジェクトを関数のように呼び出す方法を使っています。

たとえばこういう使い方があります。

>>> from operator import methodcaller
>>> l = ['a', 'b', 'c']
>>> joiner = methodcaller('join', l)
>>> joiner('-')
'a-b-c'
>>> joiner('+')
'a+b+c'

l というリストが与えられたとします。 ここで、 joiner = methodcaller('join', l) という関数を定義します。 関数を定義しているのに def を使っていないのが不思議ですね。 で、この関数に - を渡すと a-b-c が返り、 + を渡すと a+b+c が返ります。 つまり methodcaller('join', l)('-')'-'.join(l) と等価だというわけです。

同じような働きをする関数を methodcaller を使わずに自前で用意するとこんな感じでしょうか。

def joiner(s):
    return getattr(s, 'join')(l)

簡単なメソッドの適用なのでそこまで苦労しなさそうですが、 join メソッドも l も実質ベタ書きに等しいので汎用性はありません。 methodcaller を使って書いた方がコスパ良さそうですね。

ソースを読む

なんとなくソースコードが気になったので読んでみました。

全体像は こんな感じ です。

class methodcaller:
    """
    Return a callable object that calls the given method on its operand.
    After f = methodcaller('name'), the call f(r) returns r.name().
    After g = methodcaller('name', 'date', foo=1), the call g(r) returns
    r.name('date', foo=1).
    """
    __slots__ = ('_name', '_args', '_kwargs')

    def __init__(self, name, /, *args, **kwargs):
        self._name = name
        if not isinstance(self._name, str):
            raise TypeError('method name must be a string')
        self._args = args
        self._kwargs = kwargs

    def __call__(self, obj):
        return getattr(obj, self._name)(*self._args, **self._kwargs)

    def __repr__(self):
        args = [repr(self._name)]
        args.extend(map(repr, self._args))
        args.extend('%s=%r' % (k, v) for k, v in self._kwargs.items())
        return '%s.%s(%s)' % (self.__class__.__module__,
                              self.__class__.__name__,
                              ', '.join(args))

    def __reduce__(self):
        if not self._kwargs:
            return self.__class__, (self._name,) + self._args
        else:
            from functools import partial
            return partial(self.__class__, self._name, **self._kwargs), self._args

コアとなるのは __caller__ メソッドの実装ただそれだけで、先ほど書いた

def joiner(s):
    return getattr(s, 'join')(l)

    def __call__(self, obj):
        return getattr(obj, self._name)(*self._args, **self._kwargs)

の実装がほぼ同じであるところからして「たしかに」という程度なのですが、 Python でよくみるダンダーメソッド(アンダースコア二つで囲まれたメソッド)だけで構成されていて上級者感があり、せっかくなのでもう少し深追いしてみます。

__slots__ とは何か

たまに標準ライブラリのソースコードを読んでると出くわします。

ドキュメント はこちらです。 とりあえずそのまま読んでみると以下の説明があります。

  • インスタンスが用いる変数名を表す、文字列、イテラブル、または文字列のシーケンスを代入できる
  • __slots__ は、各インスタンスに対して宣言された変数に必要な記憶領域を確保し、 __dict____weakref__ が自動的に生成されないようにする
  • __dict__ 変数がない場合、 __slots__ に列挙されていない新たな変数をインスタンスに代入することはできない
  • 列挙されていない変数名を使って代入しようとしたとき、 AttributeError が送出される

簡単にいえば、クラスの属性を明示的に宣言したものだけに制限しているわけです。 いろんなところから参照されることを前提にしている場合に主に使うような感じでしょうか。

例えば次のようなクラスがあったとします。 この myclass は _hoge_fuga という属性を持っていて、 __slots__ にはこの二つの属性が宣言されています。

class myclass:
    __slots__ = ('_hoge', '_fuga')

    def __init__(self, hoge, fuga):
        self._hoge = hoge
        self._fuga = fuga

    def get_hoge(self):
        print(self._hoge)

このクラスのオブジェクトを生成して普通に使うことができます。

>>> obj = myclass("a", "b")
>>> obj.get_hoge()
a

ここで、勝手に piyo という属性を新たに定義しようとすると AttributeError が発生します。

>>> setattr(obj, "piyo", "c")
Traceback (most recent call last):
  File "slots_sample.py", line 17, in <module>
    setattr(obj, "piyo", "c")
AttributeError: 'myclass' object has no attribute 'piyo'

気になったので試してみましたが、コンストラクタに定義した属性を __slots__ に含めないでおくとどうなるでしょうか。 つまりこうしてみます

class myclass:
    __slots__ = ('_fuga') # _hoge を消してみる

    def __init__(self, hoge, fuga):
        self._hoge = hoge
        self._fuga = fuga

    def get_hoge(self):
        print(self._hoge)

オブジェクトを生成しようとするとやはり AttirbuteError が発生し、初手から詰んでるクラスになってしまいました。

>>> obj = myclass("a", "b")
Traceback (most recent call last):
  File "slots_sample.py", line 16, in <module>
    obj = myclass("a", "b")
  File "slots_sample.py", line 6, in __init__
    self._hoge = hoge
AttributeError: 'myclass' object has no attribute '_hoge'

もう一つ試してみます。また最初のような __slots__ の定義に戻します。

class myclass:
    __slots__ = ('_hoge', '_fuga')

    def __init__(self, hoge, fuga):
        self._hoge = hoge
        self._fuga = fuga

    def get_hoge(self):
        print(self._hoge)

ここで、こちら側から __slots__ そのものを書き換えてしまえないか試してみます。

>>> obj = myclass("a", "b")
>>> obj.__slots__
('_hoge', '_fuga') # 外部から読める

>>> obj.__slots__ = ('_hoge', '_fuga', 'piyo')
Traceback (most recent call last):
  File "slots_sample.py", line 18, in <module>
    obj.__slots__ = ('_hoge', '_fuga', 'piyo')
AttributeError: 'myclass' object attribute '__slots__' is read-only

この場合も AttirbuteError が発生しますね。 attribute '__slots__' is read-only と言われているのでつまりそういうことです。

methodcaller では

次のように定義されているので、これら以外の属性を持てないということになります。

__slots__ = ('_name', '_args', '_kwargs')

__init__ では何をしているか

__init__ メソッド自体はコンストラクタとしてよく見かけるものとなっています。 オブジェクト生成時にどういう属性をこちら側から渡してやらないといけないかが定義されています。

methodcaller では

    def __init__(self, name, /, *args, **kwargs):
        self._name = name
        if not isinstance(self._name, str):
            raise TypeError('method name must be a string')
        self._args = args
        self._kwargs = kwargs

_name 属性をセットし、もしそれが str 形ではないときは例外を送出します。 また、可変調引数を取れるようにもなっています。

__call__ とは何か

端的にいえばこのメソッドに定義した挙動が、生成したオブジェクトを関数として呼び出したときの挙動を定義できます

例えば次のようなクラスがあったとします。 hoge という変数を渡せばオブジェクトを生成できます。

class myclass:
    def __init__(self, hoge):
        self._hoge = hoge

    def get_hoge(self):
        print(self._hoge)

    def __call__(self):
        return self.get_hoge()

そして __call__ メソッドは get_hoge() メソッドを返すので、次のように obj() のように呼び出すと実質的に obj.get_hoge() と同じことができます。

>>> obj = myclass("a")
>>> obj()
a

関数呼び出しのように見えて実際にはオブジェクトに属するメソッドを呼び出しているので、obj() と書くだけであらかじめ与えた a という値が帰ってくるというところが特徴です。 つまりオブジェクトの内部状態を保持しているわけです。

ちなみにここで __call__ メソッドをコメントアウトして同じことをすると次のようなエラーが返ります。

Traceback (most recent call last):
  File "slots_sample.py", line 22, in <module>
    obj()
TypeError: 'myclass' object is not callable

なるほど、 callable にするには __call__ メソッドを定義しないといけないというワケなんですね。

methodcaller では

    def __call__(self, obj):
        return getattr(obj, self._name)(*self._args, **self._kwargs)

例えば先ほどの myclass クラスのオブジェクト obj を生成して、その上で get_hoge を呼び出し可能オブジェクトとして生成します。

obj = myclass('a', 'b')

get_hoge = methodcaller('get_hoge')
get_hoge(obj)

一旦 _args_kwargs についてはスルーすると、これは getattr(obj, get_hoge) と同じものになっていますので、 メソッドの呼び出しを関数呼び出しのような形で書けるわけです。

_args_kwargs が与えられている場合には obj.get_hoge(何か, 別の何か) もしくは obj.get_hoge(変数名=何か, 変数名=別の何か) という形で呼び出されます。

methodcaller クラス自体の使い道としてはこれで以上ですが、せっかくなので残りの二つのメソッドも見ていきます。

__repr__ では何をしているか

組み込み関数の repr から呼び出される特殊メソッドです。 現在のオブジェクトの状態を表すような文字列を返すように実装します。

特に対話型コンソール(REPL)で呼び出したときに暗黙的に呼び出されるものとなります。 先ほどの馬鹿みたいな myclass にも実装してみます。

まず、実装していない状態で REPL で呼び出してみるとこうなります。

>>> from slots_sample import myclass
>>> obj = myclass('a', 'b')
>>> obj
<slots_sample.myclass object at 0x1081173a0>

ここで __repr__ メソッドを実装します。

class myclass:
    __slots__ = ('_hoge', '_fuga')

    def __init__(self, hoge, fuga):
        self._hoge = hoge
        self._fuga = fuga

    def get_hoge(self):
        print(self._hoge)

    def __call__(self):
        return self.get_hoge()

    def __repr__(self):
        return f"myclass('{self._hoge}', '{self._fuga}')"

もう一度同じことをしてみます。

>>> from slots_sample import myclass
>>> obj = myclass('a', 'b')
>>> obj
myclass('a', 'b')

このようにオブジェクトをそっくり再現できるような形を返却するように実装するのが理想的です。 単純に理想的というだけでもっと適当に実装することもできます。

例えば次のようにしても(なんの役にも立ちませんが)問題なく動作します。

    def __repr__(self):
        return 'ニンニク'

>>> obj
ニンニク

ちなみに f"myclass('{self._hoge}', '{self._fuga}')" の書き方についてシングルクオートとダブルクオートを混ぜてやや強引な気がしますが、もっといい書き方があります。

    def __repr__(self):
        return 'myclass(%r, %r)' % (self._hoge, self._fuga)

最初の書き方は 'myclass(%s, %s)' % (self._hoge, self._fuga) と同じです。 なので %s ではなく %r と書くことがポイントとなります。

また、 % ではなく新しい format() を使った書き方もあります。 この場合は !r!s で使い分けることができます。

    def __repr__(self):
        return 'myclass({!r}, {!r})'.format(self._hoge, self._fuga)

この %r ないし !r%s ないし !s の挙動の違いについては ドキュメント に端的な例が載っています。

>>> "repr() shows quotes: {!r}; str() doesn't: {!s}".format('test1', 'test2')
"repr() shows quotes: 'test1'; str() doesn't: test2"

要するに r の方を使うとクオーテーションのマークごと表示させることができるわけです。 むしろこの時のために用意されているようなものです。

似たような特殊メソッドとして、 __str__ があります。 これは str() コンストラクタで呼び出されるもので、文字列として扱う時の戻り値を定義しているわけです。 print() 関数は暗黙的にこのメソッドを使っています。

ちなみに、 __str__ が実装されていなくて __repr__ だけが実装されている場合、 print() 関数は __repr__ の方を呼び出すので、基本的にはこの __repr__ だけ実装してあれば事足りることになります。

methodcaller では

    def __repr__(self):
        args = [repr(self._name)]
        args.extend(map(repr, self._args))
        args.extend('%s=%r' % (k, v) for k, v in self._kwargs.items())
        return '%s.%s(%s)' % (self.__class__.__module__,
                              self.__class__.__name__,
                              ', '.join(args))

まず name として渡されたものを repr() 組み込み関数、つまり __repr__ を作用させた結果をリストに格納します。 repr の結果を実装するのであれば引数にも repr を適用してその結果を使うというのはごく自然なやり方ですね。

次にタプルとして与えられた args については map()を使って repr() を全ての要素に適用しそれを先ほどのリストに追加します。

そして kwrgs については辞書型なので内包表記で展開して 変数名=値 の形で先ほどのリストに追加します。

最後に self.__class__.__module__ と self.__class__.__name__ でそれぞれ operatormethodcaller という文字列を取得し、 operator.methodcaller('nameの値', 変数の値, 変数名=変数の値) の形で出力してくれます。

この self.__class__.__module__ と self.__class__.__name__ という書き方は汎用性があるのでいろんなところで使われてそうですね。

__reduce__ とは何か

最後に __reduce__ です。 ドキュメント を読むと次のようなことが書いてあります。

  • 引数を取らず、文字列あるいはタプルを返す(タプルの方が望ましい)
  • 文字列が返される場合、どの文字列はグローバル変数の名前として解釈される
    • それはオブジェクトのモジュールから見たローカル名であるべきである
  • タプルが返される場合、2つから6つまでのアイテムでなければならず、順に次のような役割を果たす
    • (必須)オブジェクトの初期バージョンを作成するために呼ばれる呼び出し可能オブジェクト
    • (必須)呼び出し可能オブジェクトに対する引数のタプル
      • 呼び出し可能オブジェクトが引数を受け取らない場合は空のタプルでないといけない
    • (任意)前述のオブジェクトの __setstate()__ メソッドに渡れるオブジェクトの状態
      • オブジェクトがこのようなメソッドを持たない場合、値は辞書出なければならず、オブジェクトの __dict__ 属性に追加される
    • (任意)連続した要素を yield するイテレータ
      • obj.append(item) を使用して、あるいはバッチでは obj.extend(list_of_items) を使用してオブジェクトに追加される
      • 主にリストのサブクラスに対して使用されるが、適切なシグネチャを持つ append() および extend() メソッドがある限り、他のクラスで使用することができる
    • (任意)連続する key-value ペアを yield するイテレータ
      • obj[key] = value を使用してオブジェクトに格納される
      • 主に辞書のサブクラスに対して使用されるが、 __setitem__() を実装している限り他のクラスでも使用することができる
    • (任意)呼び出し可能な (obj, state)シグネチャ
      • この呼び出し可能オブジェクトによって obj のスタティックメソッドである __setstate__() を呼び出す代わりに、ユーザーは特定のオブジェクトの状態を更新する振る舞いをプログラマティックに制御することが可能になる
      • None 以外のとき、この呼び出し可能オブジェクトは obj__setstate__() よりも優先される

一ミリもわからないです。 なんか __setstate__() の方も読まないといけないような感じがしてめんどくさいですね。

というかそもそもどういう時に呼び出されるんでしょうか。

ドキュメント を引いて出てくるのが pickle モジュールでその関連のようですが、 pickle モジュール自体よくわかりません。

pickle モジュールは Python オブジェクトの直列化および直列化されたオブジェクトの復元のためのバイナリプロトコルを実装しています。"Pickle 化" は Python オブジェクト階層をバイトストリームに変換する処理、"非 pickle 化" は (バイナリファイル または バイトライクオブジェクト から) バイトストリームをオブジェクト階層に復元する処理を意味します。

オブジェクトを直列化??するみたいですね???

どうやら似たような系列(といっても似て非なるもの)として json モジュールがあるみたいです。 dict 形式のオブジェクトを json 形式に書き出すと単なる文字列として直列化されるが、 pickle だと Python 独自のバイナリ形式で直列化される、という風な具合のようです。なんとなくイメージがわかり始めたような気がしますね。

まぁ要するに pickle 形式に変換する時のルールを決めているようなものなんですかねぇ。知らんけど。

オブジェクトの呼び出し状態をバイナリとして外部に書き出しておくことができるようで、機械学習系の巨大なデータの読み書きが発生するようなケースにおいてよく使われるみたいです。

まぁまた必要になったときにまた見に来たらいいかなという気がします。

methodcaller では

一応雰囲気だけ見ておきます。

    def __reduce__(self):
        if not self._kwargs:
            return self.__class__, (self._name,) + self._args
        else:
            from functools import partial
            return partial(self.__class__, self._name, **self._kwargs), self._args

引数として kwargs を指定していない場合は (クラス名, (name属性の値, その他1, その他2)) のような形が返ってきます。 一方で kwargs が指定されている場合は、args 以外の変数を functools.partial() を使用し partial オブジェクトとしてひとまとめにしたうえで後ろに args をひっつけたタプルを作っています。

なんとなく直列化してるのかな〜〜〜〜みたいな雰囲気はありますが、多分知らないアニメの話になんとなく相槌を打ってるときの心理状態でしかないので、実際のところ全くわかりません。

また今度にしよう。

強引なまとめ

なんとなく見覚えのない __reduce__ メソッドが気になって読み始めたわけですが、結局ちゃんと理解する前に飽きてしまいました。

また今度何かのきっかけで使うことがあればいいですね。(なさそう)