前の記事で、Pythonのディスクリプタについて書きました。
前回の記事のままでもよいのですが、もうちょっと書き方を工夫してみます。
前回まとめ
Pythonの属性アクセス
前回からよく、
exam = Exam()
exam.math_grade = 60
というソースを載せています。
このmath_grade
にアクセスする際、どのようにそのフィールドを探しているのでしょうか?
順序としては、
- インスタンスの属性を探す(
exam.__dict__
) - クラスの属性を探す(
Exam.__dict
)
となっています。
しかし、アクセスしたmath_grade
がディスクリプタ*1である場合、インスタンス属性より優先して、ディスクリプタが使われます。
つまり、インスタンスに同名の属性があっても、明示的にアクセスしない限り使われません。
ソース:デスクリプタ HowTo ガイド — Python 3.6.5 ドキュメント
前回のディスクリプタのソース
前回、ディスクリプタの利用側で、
from grade import Grade class Exam(object): """試験結果""" math_grade = Grade("math_grade") # 以下省略
と書きました。
exam.math_grade
という記述は、先述の理由でディスクリプタの呼び出してとして処理されます。
ディスクリプタの内部、__get__
メソッドで
class Grade(object): """各試験の結果""" def __get__(self, instance, instance_type): """getter descriptor""" if instance is None: return self return instance.__dict__[self.name]
と書いて、instance.__dict__[self.name]
で、明示的にインスタンスの属性にアクセスしているため、優先順を気にせず利用できています。
dictへのアクセスを止める
とはいえ、__dict__
を直接操作するのは微妙なので、修正します。
"""GradeプロパティのDescriptor""" class Grade(object): """各試験の結果""" def __init__(self, name): """初期化""" self.name = name def __get__(self, instance, instance_type): """getter descriptor""" if instance is None: return self return getattr(instance, self.name, 0) def __set__(self, instance, value: int): """setter descriptor""" if not (0 <= value <= 100): raise ValueError("Grade must be between 0 and 100") setattr(instance, self.name, value)
標準メソッドであるgetattr
とsetattr
を使うようにします。*2
動かない
上のように書き直すと、動きません。
無限ループに陥って、止まります。*3
修正前に動いていた理由として、
明示的にインスタンスの属性にアクセスしているため
というのを挙げました。
getattr
を使うと、通常の属性アクセスとして処理されます。
その結果、
仕様でgetterが動くため、__get__
が呼ばれ続けます。
回避する
プロパティを使っていた時のように、インスタンスの属性を別名にしてしまえばいいです。
つまり、
def math_grade(self) return self._math_grade
のように、インスタンス属性を、先頭アンダーバー付きの属性名にしてしまいます。
そのためには、
"""GradeプロパティのDescriptor""" class Grade(object): """各試験の結果""" def __init__(self, name): """初期化""" self.name = name self.internal_name = "_" + name def __get__(self, instance, instance_type): """getter descriptor""" if instance is None: return self return getattr(instance, self.internal_name, 0) def __set__(self, instance, value: int): """setter descriptor""" if not (0 <= value <= 100): raise ValueError("Grade must be between 0 and 100") setattr(instance, self.internal_name, value)
ディスクリプタにinternal_name
を追加し、それでアクセスするようにします。
欠点
面倒なことです。
具体的には、このディスクリプタを利用するクラスで、
from grade import Grade class Exam(object): """試験結果""" math_grade = Grade("math_grade") writing_grade = Grade("writing_grade") science_grade = Grade("science_grade")
毎回、フィールド名を指定しなければならない点です。
わざわざ、自分のクラス属性と同じ名前を引数に指定するのは、確かに面倒です。
Effective Pythonにも、同様の欠点が挙げられています。
どうするのか
長くなるので、それはまた次回に…
こういう場合は、メタクラスを使うとよい、と紹介されています。
前回、名前を渡すの止めるべき、ということを書きましたが、次回に回します。
次の記事です。