ゆくゆくは有へと

おかゆ/彼ノ∅有生 の雑記

Effective Python 項目31 メモ ※追記 35

項目31 で、ディスクリプタがクラス属性だから各インスタンス間で共有されちゃうという問題があって、 それに対して、ディスクリプタが保持する状態を辞書にして、各インスタンスをキーとして保管しておこうっていう方策に出ているわけですが、 それだとそのインスタンスが unhashable な場合に適用できないので汎用性に欠けるのでは??と思って色々調べていた。

ディスクリプタ HowTo ガイド — Python 3.3.6 ドキュメント

ここに、Propertypython等価コードがあって、

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__に直接アクセスせずにgetattrsetattrを使っています。結果、

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()

どっちがいいのかはわかんない