読者です 読者をやめる 読者になる 読者になる

CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: 特殊メソッド __getattr__() を使う際の注意点

Python

特殊メソッドというのは、名前の前後にアンダースコアがふたつ付いた、特別な意味をもったメソッドのことをいう。 これをユーザ定義クラスで実装すると、クラスのもつ基本的な挙動をオーバーライドできる。 その中でも __getattr__() という特殊メソッドを使うと、未知のアトリビュートが呼ばれたときの挙動を定義できる。 これを活用すると、オブジェクトのラッパーが簡単に作れる。

次のサンプルコードでは MyClass クラスを Wrapper クラスがラップしている。 Wrapper クラスはラップ対象のオブジェクトをインスタンス化するときの引数として受け取る。 Wrapper クラスは __getattr__() メソッドを使うことで自身の未知のアトリビュートへのアクセスを、すべてラップ対象のオブジェクトへと委譲している。 これによって、原理的に Wrapper クラスのインスタンスは MyClass クラスのインスタンスとして振る舞うことができる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class MyClass(object):
    ''' ラップ対象のクラス '''

    def __init__(self):
        self.msg = 'Hello, World!'

    def greet(self):
        print(self.msg)

    def __str__(self):
        return self.msg


class Wrapper(object):
    ''' ラッパークラス '''

    def __init__(self, obj):
        ''' ラップ対象のクラスをインスタンス化するときに受け取る '''
        self.obj = obj

    def __getattr__(self, name):
        ''' 未知のアトリビュートへのアクセスは、この特殊メソッドが呼ばれる '''
        return getattr(self.obj, name)


def main():
    # ラップ対象のインスタンスを直接使った場合
    obj = MyClass()
    obj.greet()

    # ラップクラス経由でインスタンスを使った場合
    wrapper = Wrapper(obj)
    wrapper.greet()


if __name__ == '__main__':
    main()

上記を保存して実行してみよう。 MyClass クラスのインスタンスで直接 greet() メソッドを呼んだ場合と、Wrapper クラスのインスタンス経由で呼んだ場合で、同じ結果が得られている。

$ python wrapper.py
Hello, World!
Hello, World!

しかし、これにはちょっとした注意点もある。 次のサンプルコードでは、先ほどの内容にちょっと手を加えている。 具体的には greet() メソッドを呼ぶ代わりに print() 関数に直接オブジェクトを渡すようにした。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class MyClass(object):
    ''' ラップ対象のクラス '''

    def __init__(self):
        self.msg = 'Hello, World!'

    def greet(self):
        print(self.msg)

    def __str__(self):
        return self.msg


class Wrapper(object):
    ''' ラッパークラス '''

    def __init__(self, obj):
        ''' ラップ対象のクラスをインスタンス化するときに受け取る '''
        self.obj = obj

    def __getattr__(self, name):
        ''' 未知のアトリビュートへのアクセスは、この特殊メソッドが呼ばれる '''
        return getattr(self.obj, name)


def main():
    # ラップ対象のインスタンスを直接使った場合
    obj = MyClass()
    print(obj)

    # ラップクラス経由でインスタンスを使った場合
    wrapper = Wrapper(obj)
    print(wrapper)


if __name__ == '__main__':
    main()

上記を実行してみよう。 先ほどとは違って、両者のオブジェクトで異なる結果が得られた。

$ python wrapper.py 
Hello, World!
<__main__.Wrapper object at 0x101798dd8>

なぜこんなことが起こったのだろうか? 実は __getattr__() メソッドは、未知のアトリビュートであっても特殊メソッドの呼び出しには反応しない。 オブジェクトを print() 関数に渡した際はには、実は特殊メソッドの __str__() が呼ばれることになっている。 だから Wrapper クラスのインスタンスは特殊メソッドで __getattr__() を呼び出すことなく、自身のもつ __str__() をそのまま呼び出してしまった。

なので、いくら __getattr__() メソッドで大抵のアトリビュートへのアクセスは補足できるといっても、特殊メソッドだけは個別に定義しなきゃいけない。 次のサンプルコードでは Wrapper クラスに対して個別に __str__() メソッドを追加している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class MyClass(object):
    ''' ラップ対象のクラス '''

    def __init__(self):
        self.msg = 'Hello, World!'

    def greet(self):
        print(self.msg)

    def __str__(self):
        return self.msg


class Wrapper(object):
    ''' ラッパークラス '''

    def __init__(self, obj):
        ''' ラップ対象のクラスをインスタンス化するときに受け取る '''
        self.obj = obj

    def __getattr__(self, name):
        ''' 未知のアトリビュートへのアクセスは、この特殊メソッドが呼ばれる '''
        return getattr(self.obj, name)

    def __str__(self):
        ''' 特殊メソッドは __getattr__() で補足できないので個別に定義する必要あり '''
        return self.obj.__str__()


def main():
    # ラップ対象のインスタンスを直接使った場合
    obj = MyClass()
    print(obj)

    # ラップクラス経由でインスタンスを使った場合
    wrapper = Wrapper(obj)
    print(wrapper)


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python wrapper.py
Hello, World!
Hello, World!

今度は上手くいった。

めでたしめでたし。