CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: __getattr__() のあるオブジェクトを直列化しようとしてハマった話

今回は特殊メソッドの __getattr__() があるオブジェクトを pickle で直列化・非直列化 (SerDe) しようとしたらハマった話について。

まず、特殊メソッドの __getattr__() をクラスに実装してあると、そのインスタンスは未定義のアトリビュートにアクセスが生じたとき呼び出しがトラップされる。 そして、この __getattr__() を実装したクラスのインスタンスを pickle で SerDe しようとしたとき思わぬ挙動となった。 結論から先に述べると __getattr__() を実装してあると __getstate__()__setstate__() の呼び出しまでトラップされてしまう。 これらのメソッドは SerDe の振る舞いをオーバーライドするための特殊メソッドとなっている。 この問題の対策としては __getattr__() のある SerDe が必要なクラスには __getstate__()__setstate__() を実装しておくことが考えられる。

なお、pickle を使ったオブジェクトの SerDe の概要については、以下のエントリを参照のこと。

blog.amedama.jp

使った環境は次の通り。

$ 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!