『Effective Python』の続き。@property
の限界を超えろ。
www.effectivepython.com
@property
デコレータの欠点として再利用ができないというのがある。
テストの成績を管理するクラスを例に説明している。
class Exam(object): def __init__(self): self._writing_grade = 0 self._math_grade = 0 @staticmethod def _check_grade(value): if not (0 <= value <= 100): raise ValueError('Grade must be between 0 and 100') @property def writing_grade(self): return self._writing_grade @writing_grade.setter def writing_grade(self, value): self._check_grade(value) self._writing_grade = value @property def math_grade(self): return self._math_grade @math_grade.setter def math_grade(self, value): self._check_grade(value) self._math_grade = value
このように科目ごとに@property
を用意する必要があり、面倒である。
そこで、ディスクリプタを使って解消しようという話である。
class Grade(object): def __init__(self): self._value = 0 def __get__(self, instance, instance_type): return self._value def __set__(self, instance, value): if not (0 <= value <= 100): raise ValueError('Grade must be between 0 and 100') self._value = value class Exam(object): math_grade = Grade() writing_grade = Grade() science_grade = Grade()
とすると 一見 上手く動作するように見えるが、
first_exam = Exam() first_exam.writing_grade = 82 first_exam.science_grade = 99 print('Writing', first_exam.writing_grade) print('Science', first_exam.science_grade) second_exam = Exam() second_exam.writing_grade = 75 print('Second', second_exam.writing_grade, 'is right') print('First ', first_exam.writing_grade, 'is wrong')
Writing 82 Science 99 Second 75 is right First 75 is wrong
のようにクラス属性を使いまわしてしまい、期待していない動作をしてしまう。
じゃあどうするかというと、Exam
のインスタンスを科目ごとに管理すればよい。
class Grade(object): def __init__(self): self._values = {} def __get__(self, instance, instance_type): if instance is None: return self return self._values.get(instance, 0) def __set__(self, instance, value): if not (0 <= value <= 100): raise ValueError('Grade must be between 0 and 100') self._values[instance] = value
これで無事動作するが、self._values = {}
でインスタンスへの参照が残ってしまいメモリリークが生じてしまうと指摘している。
そこで、weakref
モジュールを使って解決しましょうと提案している。
weakref
モジュールの簡単な解説はここにある。
11. 標準ライブラリミニツアー – その 2 — Python 3.4.3 ドキュメント
Python は自動的にメモリを管理します (ほとんどのオブジェクトは参照カウント方式で管理し、ガベージコレクション(garbage collection)で循環参照を除去します)。オブジェクトに対する最後の参照がなくなってしばらくするとメモリは解放されます。
このようなアプローチはほとんどのアプリケーションでうまく動作しますが、中にはオブジェクトをどこか別の場所で利用している間だけ追跡しておきたい場合もあります。残念ながら、オブジェクトを追跡するだけでオブジェクトに対する恒久的な参照を作ることになってしまいます。 weakref モジュールでは、オブジェクトへの参照を作らずに追跡するためのツールを提供しています。弱参照オブジェクトが不要になると、弱参照 (weakref) テーブルから自動的に除去され、コールバック関数がトリガされます。弱参照を使う典型的な応用例には、作成コストの大きいオブジェクトのキャッシュがあります。
なお、『Python文法詳解』の「7.6 ディスクリプタ」には
ディスクリプタは、
__get__()
というメソッドを持つオブジェクトで、属性値としてディスクリプタが参照されると、このメソッドが呼び出され、戻り値の属性の値として返します。
とある。
また、ディスクリプタには「データディスクリプタ」と「非データディスクリプタ」があり、違いは__set__()
及び__detele__()
を実装しているかで、実装してあれば(使わないにしても)データディスクリプタとなる。
また、『Python文法詳解』の「7.6 ディスクリプタ」には
インスタンスから属性値を
obj.ATTR
として取得するとき、属性値が普通のオブジェクトか非データディスクリプタならば、値を次の順序で検索します。属性値がデータディスクリプタの場合は、次の順序で検索されます。
インスタンスに、クラスのデータディスクリプタと同盟の属性が登録されていても、常にデータディスクリプタが優先的に使用されます。 この機能を実現するために、インスタンスから属性値を取得するときは、最初に必ずクラスの名前空間を検索し、データディスクリプタが見つからなかった時のみ、 インスタンスの名前空間を検索するようになっています。
とある。 先程の 一見 上手く動作する実装は、ディスクリプタのこの振る舞いによって引き起こされたと思われる。
こう、『Python文法詳解』や『プログラミング言語C』や『Cリファレンスマニュアル』のような本はこのようなときに役に立つ。 何故か上手くいかないからこうしましょう、と言われても納得できないときはこのようなリファレンスが便利である。