cover_effective_python

【 Effective Python, 2nd Edition 】__getattr__、__getattribute__、__setattr__ メソッドを利用して、事前に定義していないインスタンス属性を操作しよう! 投稿一覧へ戻る

Tags: Python , Effective , attribute , property , __getattr__ , __getattribute__ , __setattr__ , descriptor

Published 2020年7月18日11:07 by T.Tsuyoshi

データベースのレコードに対応する Python クラスを考えます。


このとき、クラスの各属性はレコードの各フィールドに対応していますね。


つまり、前もって対象とするテーブルの構造が分かっていなければそれに対応するクラスなんか作れないよ、ということです。


ところが Python では「えっ、前もって分からないの?それじゃしょうがないからとにかく始めちゃって分かったら追加すれば?」という大雑把というか、ジェネリック ( generic ) というか、そのようなアプローチも可能なんです。


こんなことがどうやって可能になっているんでしょうか?


属性アクセスを操作することが可能な @property デコレータ ( decorator ) にしても、デスクリプタ ( descriptor ) にしても、前もって定義しておかなければ使いようがありません。


ジャジャーーン!


Python においてクラスインスタンス属性に対する動的な操作を可能にしているのは __getattr__ 特殊関数です。


もしクラスで __getattr__ メソッドが定義されている場合、インスタンス辞書に含まれていない属性へのアクセスが生じるとその度に __getattr__ メソッドが呼び出されることになっているんです。


class LazyRecord:
def __init__(self):
self.exists = 5

def __getattr__(self, item):
value = f"これは {item} の値です"
setattr(self, item, value)
return value



LazyRecord クラスでは定義されていない属性: boo にアクセスしてみましょう。


data = LazyRecord()


print(f"Before: {data.__dict__}")
# Before: {'exists': 5}

print(f"存在しない boo 属性にアクセスしました: {data.boo}")
# 存在しない boo 属性にアクセスしました: これは boo の値です

print(f"After: {data.__dict__}")
# After: {'exists': 5, 'boo': 'これは boo の値です'}



インスタンス属性の辞書に 'boo' が追加されていますね。


では、もう少し理解を深めるために、どんなタイミングで __getattr__ メソッドが呼び出されているのかを見てみましょう。


class LoggingLazyRecord(LazyRecord):
def __getattr__(self, item):
print(f"__getattr__({item!r}) 呼び出しが発生、{item!r} キーをインスタンス辞書に追加しました。")
result = super().__getattr__(item)
print(f"追加した {item!r} キーの値: {result!r}")
return result



サブクラスの __getattr__ メソッドの中で親クラスの __getattr__ メソッドを呼び出さなければいけません。
setattr() でインスタンス属性の辞書に追加しているのは親クラスの __getattr__ の中ですから、呼び出さないといつまでたっても辞書に追加されません。


data = LoggingLazyRecord()

print(f"exists: {data.exists}")
# exists: 5

print(f"最初の boo アクセス: {data.boo!r}")
# __getattr__('boo') 呼び出しが発生、'boo' キーをインスタンス辞書に追加しました。
# 追加した 'boo' キーの値: 'これは boo の値です'
# 最初の boo アクセス: 'これは boo の値です'


print(f"2回目の boo アクセス: {data.boo!r}")
# 2回目の boo アクセス: 'これは boo の値です'



最初の data.exists のコールでは、exists はもともとインスタンス属性の辞書に含まれていますから __getattr__ は呼び出されません。


続く data.boo では、boo が辞書に含まれていないため __getattr__ が呼ばれ、setattr() によってインスタンス属性の辞書に追加されます。


2回目の data.boo では、最初のコールで boo が属性辞書に追加されていますから、__getattr__ が呼ばれることなく終了しています。


この例のように、__getattr__ 特殊関数は、インスタンス属性辞書への動的追加を可能にし、2回目以降の低コストでのアクセスを保証してくれます。


ですが、属性に対するアクセスがあるたびに、データベースのデータとインスタンス属性の値が一致しているかを確認したうえで何らかの処理を行いたい、というような場合、__getattr__ 特殊関数では対応できません。2回目以降のアクセスでは呼び出されませんから。


このようなシナリオで利用できるのが __getattribute__ 特殊関数です。


__getattribute__ 特殊関数は、対象の属性がインスタンス辞書に含まれていようが無かろうがそれにはお構いなく、属性へのアクセスがある度に毎回呼び出されます。


class ValidatingRecord:
def __init__(self):
self.exists = 5

def __getattribute__(self, item):
print(f"__getattribute__({item!r}) 呼び出しが発生しました")
try:
value = super().__getattribute__(item)
print(f"キー [{item!r}] はインスタンス属性辞書にすでに含まれています。値: {value!r}")
return value
except AttributeError:
value = f"これは {item} の値です"
print(f"追加した {item!r} キーの値: {value!r}")
setattr(self, item, value)
return value



__getattribute__() で親クラス (ここでは object クラス) の __getattribute__() の呼び出しを try ブロックで囲んでいます。


これは、__getattr__()、__getattribute__() において、インスタンス属性辞書内に指定されたキーが存在しない場合は AttributeError 例外を発生させる、というのが Python におけるデフォルトの動作 であり、今回はそれを利用して辞書に属性を追加するようにしているためです。


data = ValidatingRecord()


print(f"exists: {data.exists}")
# exists: 5

print(f"最初の boo アクセス: {data.boo!r}")
# __getattribute__('boo') 呼び出しが発生しました
# 追加した 'boo' キーの値: 'これは boo の値です'
# 最初の boo アクセス: 'これは boo の値です'


print(f"2回目の boo アクセス: {data.boo!r}")
# __getattribute__('boo') 呼び出しが発生しました
# キー ['boo'] はインスタンス属性辞書にすでに含まれています。値: 'これは boo の値です'
# 2回目の boo アクセス: 'これは boo の値です'



__getattribute__ 特殊関数は属性アクセスのたびに毎回呼び出される、ということを忘れないでください。
インスタンス属性辞書へのアクセス、参照、必要があれば追加、といった処理はかなりのオーバーヘッドを伴い、パフォーマンス的には大きなマイナス要因になります。


また、同じクラスに __getattribute__() と __getattr__() が両方とも実装されている場合は、__getattribute__() 内で明示的に呼び出すか AttributeError 例外を発生させない限り __getattr__() は呼び出されません


さて、__getattr__() や __getattribute__() と同様にインスタンス属性辞書を参照する関数としては組み込み関数の hasattr() があります。


hasattr() は指定された属性がインスタンス辞書内に存在すれば True を、存在しなければ False を返しますが、その判断基準となっているのは getattr() によるその属性値の取出しです。


もし getattr() が「そんな属性ないでござる」とAttributeError 例外を投げてくれば False を返すんです。


ですが、そのインスタンスのクラスに __getattr__() が定義されていると、getattr() が AttributeError 例外を投げた時点で __getattr__() が呼ばれますから hasattr() が結果を返すのはその後、ということになります。


data = LoggingLazyRecord()


print(f"Before: {data.__dict__}")
# Before: {'exists': 5}

print(f"(1回目) hasattr() で 'boo' 属性の有無を確認します: {hasattr(data, 'boo')}")
# __getattr__('boo') 呼び出しが発生、'boo' キーをインスタンス辞書に追加しました。
# 追加した 'boo' キーの値: 'これは boo の値です'
# (1回目) hasattr() で 'boo' 属性の有無を確認します: True


print(f"After: {data.__dict__}")
# After: {'exists': 5, 'boo': 'これは boo の値です'}

print(f"(2回目) hasattr() で 'boo' 属性の有無を確認します: {hasattr(data, 'boo')}")
# (2回目) hasattr() で 'boo' 属性の有無を確認します: True



LoggingLazyRecord() には __getattr__() が実装されていますから、'boo' 属性を最初に参照したときには、

1: hasattr(data, 'boo') が getattr(data, 'boo') を実行
2: 'boo' 属性はインスタンスの辞書に存在しないため getattr() は AttributeError 例外を送出
3: LoggingLazyRecord クラスの __getattr__() が呼ばれて、結果的に 'boo' 属性をインスタンス属性辞書に追加、値を返す
4: 値が返ってきたので hasattr() は True を返す

という流れで処理が進みました。


ただし2回目の hasattr(data, 'boo') 実行時点ではインスタンス辞書内に 'boo' キーが存在していますから __getattr__() は実行されていません。


では、__getattribute__() を実装しているクラスでも hasattr() の動きを追ってみましょう。


data = ValidatingRecord()


print(f"(1回目) hasattr() で 'boo' 属性の有無を確認します: {hasattr(data, 'boo')}")
# __getattribute__('boo') 呼び出しが発生しました
# 追加した 'boo' キーの値: 'これは boo の値です'
# (1回目) hasattr() で 'boo' 属性の有無を確認します: True


print(f"(2回目) hasattr() で 'boo' 属性の有無を確認します: {hasattr(data, 'boo')}")
# __getattribute__('boo') 呼び出しが発生しました
# キー ['boo'] はインスタンス属性辞書にすでに含まれています。値: 'これは boo の値です'
# (2回目) hasattr() で 'boo' 属性の有無を確認します: True



__getattribute__() は、インスタンス属性辞書内に該当する属性が含まれるか含まれないかに関わらず呼び出されているのか分かると思います。


さてここで、データベースレコードに対応している Python オブジェクトの値が更新されるたびに、データベースの値も更新する処理を実装するとしましょう。


インスタンス属性への値の代入のたびに呼び出されるのは __setattr__() 特殊関数です。


属性値の取得時は __getattr__()、__getattribute__() という2つの特殊関数が用意されていましたが、代入時には __setattr__() 特殊関数しかありません。そして、常に呼び出されます。


class SaveToRecord:
def __setattr__(self, key, value):
# データベースへの処理を行います
# ...
super().__setattr__(key, value)


class LoggingSaveToRecord(SaveToRecord):
def __setattr__(self, key, value):
print(f"__setattr__({key!r}, {value!r}) 呼び出しが発生しました")
super().__setattr__(key, value)


data = LoggingSaveToRecord()


print(f"Before: {data.__dict__}")
# Before: {}


data.boo = 5


print(f"After: {data.__dict__}")
# __setattr__('boo', 5) 呼び出しが発生しました
# After: {'boo': 5}



data.boo = 7


print(f"Finally: {data.__dict__}")
# __setattr__('boo', 7) 呼び出しが発生しました
# Finally: {'boo': 7}



__getattribute__() 特殊関数と __setattr__() 特殊関数の実装時に注意しなければいけないことがあります。


それは、この2つの特殊関数が、インスタンス属性アクセス時に常に呼び出される、ということです。
ここまでも何回か書いてきたことですが、実はこれが結構曲者だったりします。


class WeirdDictionaryRecord:
def __init__(self, item):
self._item = item

def __getattribute__(self, name):
print(f"__getattribute__({name!r}) 呼び出しが発生しました")
return self._item[name]



ここではただ単純に、インスタンス属性である _item 辞書の値を取得しようとしているだけなのですが...


data = WeirdDictionaryRecord({'boo': 3})


print(data.boo)
# __getattribute__('boo') 呼び出しが発生しました
# __getattribute__('_item') 呼び出しが発生しました
# __getattribute__('_item') 呼び出しが発生しました
# ...
# Traceback...
# RecursionError: maximum recursion depth exceeded while calling a Python object



__getattribute__() の中で self._item にアクセスしているがために __getattribute__() が呼び出され、その中で self._item に...
ということで、スタックがいっぱいになるまでループした挙句お亡くなりになってしまいます。


これを解決する方法は、__getattribute__() 内でインスタンス属性辞書から値を取得する際に super().__getattribute__() を利用する ことです。
これにより __getattribute__() の再帰的な呼び出しを防止することができるようになります。


class DictionaryRecord:
def __init__(self, item):
self._item = item

def __getattribute__(self, name):
print(f"__getattribute__({name!r}) 呼び出しが発生しました")
result = super().__getattribute__('_item')
return result[name]


data = DictionaryRecord({'boo': 3})


print(data.boo)
# __getattribute__('boo') 呼び出しが発生しました
# 3



__setattr__() を実装する際も同様で、__setattr__() による再帰呼び出しを防止するために super().__setattr__() を利用してインスタンス属性辞書に値を書き込む 必要があります。


まとめ:

1: __getattr__、__setattr__ を実装することで、事前に定義されていないインスタンス属性に対する操作を行うことが可能になります。

2: __getattr__ はインスタンス属性辞書に指定された属性が存在しない場合のみ呼び出されるのに対し、__getattribute__ はインスタンス属性へのアクセスに対して常に呼び出されます。

3: __getattribute__ および __setattr__ による無限再帰呼び出しを防止するために、必ず基底クラス (例えば object クラス) の同名のメソッドを呼び出してインスタンス属性にアクセスしなければなりません。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)