今回は特殊メソッドの __getattr__()
があるオブジェクトを pickle で直列化・非直列化 (SerDe) しようとしたらハマった話について。
まず、特殊メソッドの __getattr__()
をクラスに実装してあると、そのインスタンスは未定義のアトリビュートにアクセスが生じたとき呼び出しがトラップされる。
そして、この __getattr__()
を実装したクラスのインスタンスを pickle で SerDe しようとしたとき思わぬ挙動となった。
結論から先に述べると __getattr__()
を実装してあると __getstate__()
と __setstate__()
の呼び出しまでトラップされてしまう。
これらのメソッドは SerDe の振る舞いをオーバーライドするための特殊メソッドとなっている。
この問題の対策としては __getattr__()
のある SerDe が必要なクラスには __getstate__()
と __setstate__()
を実装しておくことが考えられる。
なお、pickle を使ったオブジェクトの SerDe の概要については、以下のエントリを参照のこと。
使った環境は次の通り。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.14.6 BuildVersion: 18G84 $ python -V Python 3.7.4
特殊メソッド __getattr__()
がないときの振る舞いについて
まずは __getattr__()
を実装していないクラスを直列化・非直列化 (SerDe) してみる。
以下のサンプルコードでは Example
というクラスのインスタンスをバイト列にしてから元のオブジェクトに戻している。
#!/usr/bin/env python # -*- coding: utf-8 -*- import pickle class Example(object): """SerDe されるクラス""" def __init__(self, message): self.message = message def greet(self): print('Hello, {msg}!'.format(msg=self.message)) def main(): # Example クラスをインスタンス化する o = Example('World') # メソッドを呼び出す o.greet() # オブジェクトをバイト列にシリアライズする # このときオブジェクトに __getstate__() があれば呼ばれる # このサンプルコードにはないためデフォルトの振る舞いになる s = pickle.dumps(o) # バイト列からオブジェクトをデシリアライズする # このときオブジェクトに __setstate__() があれば呼ばれる # このサンプルコードにはないためデフォルトの振る舞いになる restored_o = pickle.loads(s) # 復元したオブジェクトのメソッドを呼び出す restored_o.greet() if __name__ == '__main__': main()
上記を実行した結果が次の通り。 ちゃんとオブジェクトをバイト列にして、また元のオブジェクトに戻せていることがわかる。
$ python serde1.py Hello, World! Hello, World!
特殊メソッド __getattr__()
があるときの振る舞いについて
続いては __getattr__()
のあるオブジェクトを SerDe してみる。
ただ、先ほどの Example
クラスに直接 __getattr__()
を追加するのはユースケースとして考えにくいので、ちょっとアレンジを加えてある。
Example
クラスはそのままに、そのラッパーとして動作する Wrapper
クラスを用意して、そこに __getattr__()
メソッドを実装した。
こういったプロキシのようなクラスは、プロキシする先のオブジェクトの呼び出しを中継するために __getattr__()
を使うことが多い。
このような状況で Wrapper
クラスのインスタンスを SerDe すると上手くいかない、というのが今回の本題となる。
#!/usr/bin/env python # -*- coding: utf-8 -*- import pickle class Wrapper(object): """別のオブジェクトへのラッパーとして動作するクラス (SerDe される)""" def __init__(self, wrap_target): self.wrap_target = wrap_target def __getattr__(self, item): """未定義のアトリビュートへのアクセスをトラップする""" def _wrapper(*args, **kwargs): print('trapped undefined access:', item) # ラップするオブジェクトのアトリビュートを取得して呼び出す func = getattr(self.wrap_target, item) return func(*args, **kwargs) return _wrapper class Example(object): """Wrapper 経由で呼び出されるクラス""" def __init__(self, message): self.message = message def greet(self): print('Hello, {msg}!'.format(msg=self.message)) def main(): o = Example('World') # Wrapper でオブジェクトをラップする w = Wrapper(o) # ラッパー経由でメソッドを呼び出す w.greet() # XXX: __getstate__() が __getattr__() 経由で呼ばれようとする s = pickle.dumps(w) # XXX: __setstate__() が __getattr__() 経由で呼ばれようとする restored_w = pickle.loads(s) restored_w.greet() if __name__ == '__main__': main()
上記を実行してみよう。
すると、次のように直列化するタイミングでエラーになる。
見ると __getstate__()
が Example
のオブジェクトにない、という内容のようだ。
$ python serde2.py trapped undefined access: greet Hello, World! trapped undefined access: __getstate__ Traceback (most recent call last): File "serde2.py", line 50, in <module> main() File "serde2.py", line 41, in main s = pickle.dumps(w) File "serde2.py", line 17, in _wrapper func = getattr(self.wrap_target, item) AttributeError: 'Example' object has no attribute '__getstate__'
Example
クラスに __*state__()
を実装すれば解決...しない
では、エラーメッセージに習って Example
クラスに __getstate__()
と __setstate__()
を実装すれば解決するだろうか?
試してみよう。
#!/usr/bin/env python # -*- coding: utf-8 -*- import pickle class Wrapper(object): """別のオブジェクトへのラッパーとして動作するクラス (SerDe される)""" def __init__(self, wrap_target): self.wrap_target = wrap_target def __getattr__(self, item): """未定義のアトリビュートへのアクセスをトラップする""" def _wrapper(*args, **kwargs): print('trapped undefined access:', item) func = getattr(self.wrap_target, item) return func(*args, **kwargs) return _wrapper class Example(object): """Wrapper 経由で呼び出されるクラス""" def __init__(self, message): self.message = message def greet(self): print('Hello, {msg}!'.format(msg=self.message)) def __getstate__(self): """__getstate__() を明示的に定義する""" return self.__dict__.copy() def __setstate__(self, state): """__setstate__() を明示的に定義する""" self.__dict__.update(state) def main(): o = Example('World') w = Wrapper(o) w.greet() # XXX: __getstate__() が __getattr__() 経由で呼ばれようとする s = pickle.dumps(w) # XXX: __setstate__() が __getattr__() 経由で呼ばれようとする restored_w = pickle.loads(s) restored_w.greet() if __name__ == '__main__': main()
残念ながら、今度は以下のようなエラーになる。
そもそも SerDe したいのは Wrapper
クラスのインスタンスなので Example
クラスに実装しても解決できない。
$ python serde3.py trapped undefined access: greet Hello, World! trapped undefined access: __getstate__ trapped undefined access: __setstate__ Traceback (most recent call last): File "serde3.py", line 56, in <module> main() File "serde3.py", line 50, in main restored_w = pickle.loads(s) File "serde3.py", line 17, in _wrapper func = getattr(self.wrap_target, item) AttributeError: 'function' object has no attribute '__setstate__'
このときのエラーメッセージがまた分かりにくくて、どうして function
オブジェクトでエラーになるんだ、となる。
Wrapper
クラスに __*state__()
を実装してみる
ということで、今度は Wrapper
クラスの方に __getstate__()
と __setstate__()
を実装してみよう。
#!/usr/bin/env python # -*- coding: utf-8 -*- import pickle class Wrapper(object): """別のオブジェクトへのラッパーとして動作するクラス (SerDe される)""" def __init__(self, wrap_target): self.wrap_target = wrap_target def __getattr__(self, item): """未定義のアトリビュートへのアクセスをトラップする""" def _wrapper(*args, **kwargs): print('trapped undefined access:', item) func = getattr(self.wrap_target, item) return func(*args, **kwargs) return _wrapper def __getstate__(self): """__getstate__() を明示的に定義する""" return self.__dict__.copy() def __setstate__(self, state): """__setstate__() を明示的に定義する""" self.__dict__.update(state) class Example(object): """Wrapper 経由で呼び出されるクラス""" def __init__(self, message): self.message = message def greet(self): print('Hello, {msg}!'.format(msg=self.message)) def main(): o = Example('World') w = Wrapper(o) w.greet() # __getstate__() が明示的に定義されているため __getattr__() は呼ばれない s = pickle.dumps(w) # __setstate__() が明示的に定義されているため __getattr__() は呼ばれない restored_w = pickle.loads(s) restored_w.greet() if __name__ == '__main__': main()
今度は次のようにエラーにならず SerDe できた。
Wrapper
クラスに __getstate__()
と __setstate__()
が定義されているため、呼び出しが __getattr__()
にトラップされることがない。
$ python serde4.py trapped undefined access: greet Hello, World! trapped undefined access: greet Hello, World!
サードパーティーのライブラリで問題が発生しているとき
先ほどのようにクラスにメソッドを定義して救えるのは、自分で定義したクラスで問題が発生している場合に限られる。 もし、サードパーティ製のライブラリで同様の問題が生じた場合には、どのような解決策があるだろうか。 幸いなことに Python は既存のクラスにも動的にメソッドを追加できる。
以下のサンプルコードでは SerDe する直前で対象のクラスに __getstate__()
と __setstate__()
を動的に追加している。
#!/usr/bin/env python # -*- coding: utf-8 -*- import pickle class Wrapper(object): """別のオブジェクトへのラッパーとして動作するクラス (SerDe される)""" def __init__(self, wrap_target): self.wrap_target = wrap_target def __getattr__(self, item): """未定義のアトリビュートへのアクセスをトラップする""" def _wrapper(*args, **kwargs): print('trapped undefined access:', item) func = getattr(self.wrap_target, item) return func(*args, **kwargs) return _wrapper class Example(object): """Wrapper 経由で呼び出されるクラス""" def __init__(self, message): self.message = message def greet(self): print('Hello, {msg}!'.format(msg=self.message)) def main(): o = Example('World') w = Wrapper(o) w.greet() # オブジェクトに __getstate__() を動的に追加する def __getstate__(self): return self.__dict__.copy() setattr(Wrapper, '__getstate__', __getstate__) # オブジェクトに __setstate__() を動的に追加する def __setstate__(self, state): self.__dict__.update(state) setattr(Wrapper, '__setstate__', __setstate__) s = pickle.dumps(w) restored_w = pickle.loads(s) restored_w.greet() if __name__ == '__main__': main()
上記を実行してみよう。 ちゃんと SerDe できていることがわかる。
$ python serde5.py trapped undefined access: greet Hello, World! trapped undefined access: greet Hello, World!

スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログ (1件) を見る