はじめに
最近は読んだ本、しかも技術書以外の小説を読んだ感想記事ばかり書いていて、エンジニアブログ感がなくなってしまったので無理矢理ネタを見つけて書きます。
operator モジュールの methodcaller 関数のソースコードを読む
まずは使い方を知る
Python 標準モジュールに operator モジュール があります。 簡単にいえば operator (演算子)を関数の形で表すモジュールです。
この辺りを使いこなせば関数型プログラミングのようなことができます。
単純なところでいえば a + b
を add(a, b)
と書けたり、a - b
を sub(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__
でそれぞれ operator
と methodcaller
という文字列を取得し、 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__
メソッドが気になって読み始めたわけですが、結局ちゃんと理解する前に飽きてしまいました。
また今度何かのきっかけで使うことがあればいいですね。(なさそう)