Effective Python 項目31 メモ ※追記 35
項目31 で、ディスクリプタがクラス属性だから各インスタンス間で共有されちゃうという問題があって、 それに対して、ディスクリプタが保持する状態を辞書にして、各インスタンスをキーとして保管しておこうっていう方策に出ているわけですが、 それだとそのインスタンスが unhashable な場合に適用できないので汎用性に欠けるのでは??と思って色々調べていた。
ディスクリプタ HowTo ガイド — Python 3.3.6 ドキュメント
ここに、Property
のpython等価コードがあって、
class Property(object): "Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError("unreadable attribute") return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError("can't set attribute") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError("can't delete attribute") self.fdel(obj) def getter(self, fget): return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset): return type(self)(self.fget, fset, self.fdel, self.__doc__) def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__)
これと Effective Python の方策を見比べるに、なるほど、__get__
や __set__
はインスタンスから呼ばれると引数にそのインスタンスを取るんだから、インスタンスごとにプライベート変数をごにょごにょしてやろうとすれば property を使う程度には汎用性が出るんですね。
と思ったけど、これだと複数の属性に同じディスクリプタを適用できないのか…。プロパティだと個々の属性に対して作っていくから、その辺りは回避される(代わりに、Effective Pythonで問題提起しているように、同じ形式が複数の属性に現れてもそれを共通化できない)。
とはいえ、結局、再利用可能な@propertyを再利用できるようにしておきたいというのがこの項目のテーマだし、考えれば、似た形式が複数の属性に現れたとしても、
- 個々で使用するプライベート変数は異なるが、
- 各プロパティメソッドの振る舞いは(プライベート変数を除き)同じ
なわけで、差異は引数として汲み取ればいいのでは?というわけで、こういうのを考えた。
class Grade(): def __init__(self, attr=""): self.attr = ("_" if attr else "") + attr + "_grade" def __get__(self, instance, instance_type): if instance is None: return self return instance.__dict__[self.attr] def __set__(self, instance, value): if not (0 <= value <= 100): raise ValueError() instance.__dict__[self.attr] = value class Exam(): math_grade = Grade("math") writing_grade = Grade("writing") science_grade = Grade("science") class Homework(): grade = Grade() if __name__ == '__main__': first_exam = Exam() first_exam.writing_grade = 82 first_exam.science_grade = 89 print("writing :", first_exam.writing_grade) print("science :", first_exam.science_grade) try: first_exam.math_grade = 101 print("math :", first_exam.math_grade) except: print("expected error") print("first_exam.__dict__:", first_exam.__dict__) galileo = Homework() galileo.grade = 95 print("galileo.__dict__:", galileo.__dict__)
Gradeディスクリプタには、そのインスタンスのどんな属性を操作するかを知らせておく必要がある。
共通のうちの差異を引数に吸収したっていうだけの、素朴なアイデア。
> python effective_python_31.py writing : 82 science : 89 expected error first_exam.__dict__: {'_science_grade': 89, '_writing_grade': 82} galileo.__dict__: {'_grade': 95}
ディスクリプタ内の辞書で管理するのに比べると、インスタンスの属性空間が汚くなるけど、でもこれは @property 使っても起こることだし。
個人的には、登録する属性名を Grade の引数に持ってこれるような仕組みがあればいいなと思うんだけど思いつかない。これができたら内部で扱う変数がミスで衝突するというようなことが(表層レベル以外で)ないだろうし。
追記
項目35「クラス属性をメタクラスで注釈する」で上で書いたのとほとんど同じものがあった。その上、最後の「登録する属性名を Grade の引数に持ってこれるような仕組みがあればいいな」に対する答えでもあった。なんだよもっと早く言ってくれよ~~
class Meta(type): def __new__(meta, name, bases, class_dict): for key, value in class_dict.items(): if isinstance(value, Grade2): value.name = key value.internal_name = '_' + key cls = type.__new__(meta, name, bases, class_dict) return cls class Grade2(): def __init__(self): self.name = None self.internal_name = None def __get__(self, instance, instance_type): if instance is None: return self return getattr(instance, self.internal_name) def __set__(self, instance, value): if not (0 <= value <= 100): raise ValueError() setattr(instance, self.internal_name, value) class Exam2(metaclass=Meta): math_grade = Grade2() writing_grade = Grade2() science_grade = Grade2() if __name__ == '__main__': print("Exam2") exam = Exam2() exam.writing_grade = 82 exam.science_grade = 89 print("writing :", exam.writing_grade) print("science :", exam.science_grade) try: exam.math_grade = 101 print("math :", exam.math_grade) except: print("expected error") print("first_exam.__dict__:", exam.__dict__)
項目35の書き方に則って、__dict__
に直接アクセスせずにgetattr
とsetattr
を使っています。結果、
Exam2 writing : 82 science : 89 expected error first_exam.__dict__: {'_science_grade': 89, '_writing_grade': 82}
となる。Exam2
だけを見れば間違いなくこっちのほうが無駄がなくていいですね。
でも個人的にはGrade2
の__init__
でわざわざ None 割り当てないといけないというところと、属性がMeta
側でごちゃごちゃされるというところで葛藤があります。カッコいいのはメタクラス使うほうですけどね。
さらに追記
メタクラスを使わずに、クラスデコレータを使う方法もあるんだね。
def grade_decorator(cls): for key, value in cls.__dict__.items(): if isinstance(value, Grade2): value.name = key value.internal_name = '_' + key return cls @grade_decorator class Exam3(): math_grade = Grade2() writing_grade = Grade2() science_grade = Grade2()
どっちがいいのかはわかんない