何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

『Effective Python』Item 32: 属性の遅延評価には__getattr__, __getattribute__,__setattr__を使おう

『Effective Python』の続き。ナマケモノでいこう。 www.effectivepython.com

遅延評価を__getattr__getattribute____setattr__で実現できますよ、という話。 __getattr__らは一般に、クラスの属性アクセスを制御するための特殊メソッドで、その具体的な応用例の項目である。

class LazyDB(object):
    def __init__(self):
        self.exists = 5

    def __getattr__(self, name):
        value = 'Value for %s' % name
        setattr(self, name, value)
        return value

class LoggingLazyDB(LazyDB):
    def __getattr__(self, name):
        print('Called __getattr__(%s)' % name)
        return super().__getattr__(name)

data = LoggingLazyDB()
print('exists:', data.exists)
print('foo:   ', data.foo)
print('foo:   ', data.foo)

のように、__getattr__が実装されたクラスのインスタンスに存在しない属性へアクセスした場合、__getattr__が実行される。 つまり、クラスのインスタンス生成時にすべての属性を保持しようとすると重たくなるようなものでも呼び出されてから作るようにすれば何かと経済的にできるよ、という話である。

__getattr__に似た特殊メソッドとして__getattribute__がある。 __getattr__存在しない属性へアクセスした場合 に実行されるが、 __getattribute__アクセス時毎回 実行される。

class ValidatingDB(object):
    def __init__(self):
        self.exists = 5

    def __getattribute__(self, name):
        print('Called __getattribute__(%s)' % name)
        try:
            return super().__getattribute__(name)
        except AttributeError:
            value = 'Value for %s' % name
            setattr(self, name, value)
            return value

data = ValidatingDB()
print('exists:', data.exists)
print('foo:   ', data.foo)
print('foo:   ', data.foo)

__setattr__も同様に扱える。こちらも アクセス時毎回 実行される。

class SavingDB(object):
    def __setattr__(self, name, value):
        # Save some data to the DB log
        pass
        super().__setattr__(name, value)

class LoggingSavingDB(SavingDB):
    def __setattr__(self, name, value):
        print('Called __setattr__(%s, %r)' % (name, value))
        super().__setattr__(name, value)

data = LoggingSavingDB()
print('Before: ', data.__dict__)
data.foo = 5
print('After:  ', data.__dict__)
data.foo = 7
print('Finally:', data.__dict__)

注意点として、__getattribute____setattr__内で直接self.valueを扱おうとすると 無限ループ に陥るのでsuper().__setattr__(self, value)など 親クラスから__getattribute____setattr__を呼び出して設定すること、とある。 この話は3. データモデル — Python 3.4.3 ドキュメントにも触れられている。

このメソッドは (計算された) 属性値を返すか、 AttributeError 例外を送出します。このメソッド再帰的に際限なく呼び出されてしまうのを防ぐため、実装の際には常に、必要な属性全てへのアクセスで、例えば object.getattribute(self, name) のように基底クラスのメソッドを同じ属性名を使って呼び出さなければなりません。

3. データモデル — Python 3.4.3 ドキュメント

また、『Effective Python』では触れられていないが、公式ドキュメントや『Python文法詳解』には__getattribute____getattr__が呼び出されない例外的な状況が説明されている。

公式ドキュメントには注釈として書かれている。

言語構文や組み込み関数から暗黙に呼び出された特殊メソッドの検索では、このメソッドも回避されることがあります。

3. データモデル — Python 3.4.3 ドキュメント

Python文法詳解』には具体例も交えて説明している。

パフォーマンス上の理由から、特殊メソッドが暗黙的に呼び出される場合は、getattribute()/getattr()メソッドは呼び出されません。 例えば、spam + hamという式を評価するとspam.add(ham)呼び出されますが、このとき、addメソッドはgetattribute`メソッドを経由せず、直接実行されます。

Python文法詳解』7.8.11 属性アクセス

属性にまつわる項目はこれで最後。次回からはメタクラスへ。

『Effective Python』Item 33: メタクラスを使ってサブクラスの検証をしよう

『Effective Python』の続き。水際で防げ。 www.effectivepython.com

『Effective Python』のChapter 4冒頭に筆者は次のように述べている。

Metaclass are often mentioned in lists of Python's features, but few understand what they accomplish in practice.

『Effective Python』 Chapter 4

Pythonの特徴の1つによくメタクラスが挙げられますが、実際にどう使うのかを理解している人は少ないです。」と訳せるだろうか。*1 そこで筆者はメタクラスの応用例としてサブクラスの検査、登録、注釈を挙げている。 今回のItem 33はサブクラスの検査を取り上げている。

下記の例は、多角形をPythonのクラスで構成する例である。 多角形の厳密な定義は真面目な幾何学の本に任せるとして、頂点が3つ以上であることを検査する機能をメタクラスに織り込んでいる。

class ValidatePolygon(type):
    def __new__(meta, name, bases, class_dict):
        # Don't validate the abstract Polygon class
        if bases != (object,):
            if class_dict['sides'] < 3:
                raise ValueError('Polygons need 3+ sides')
        return type.__new__(meta, name, bases, class_dict)

class Polygon(object, metaclass=ValidatePolygon):
    sides = None  # Specified by subclasses

    @classmethod
    def interior_angles(cls):
        return (cls.sides - 2) * 180

class Triangle(Polygon):
    sides = 3

ここで、多角形だといっているのに直線を定義しようとして、

class Line(Polygon):
    print('Before sides')
    sides = 1
    print('After sides')

とするときちんとValueErrorが送出される。

*1:accomplishは達成とかそういう意味であるが…