SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

Pythonのディスクリプタについて:3.ディスクリプタ内のアクセス改善

前の記事で、Pythonディスクリプタについて書きました。

hiroronn.hatenablog.jp

hiroronn.hatenablog.jp

前回の記事のままでもよいのですが、もうちょっと書き方を工夫してみます。

前回まとめ

Pythonの属性アクセス

前回からよく、

exam = Exam()
exam.math_grade = 60

というソースを載せています。

このmath_gradeにアクセスする際、どのようにそのフィールドを探しているのでしょうか?


順序としては、

  1. インスタンスの属性を探す(exam.__dict__
  2. クラスの属性を探す(Exam.__dict

となっています。

しかし、アクセスしたmath_gradeディスクリプタ*1である場合、インスタンス属性より優先してディスクリプタが使われます。
つまり、インスタンスに同名の属性があっても、明示的にアクセスしない限り使われません。

ソース:デスクリプタ HowTo ガイド — Python 3.6.3 ドキュメント

前回のディスクリプタのソース

前回、ディスクリプタの利用側で、

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)

標準メソッドであるgetattrsetattrを使うようにします。*2

組み込み関数:getattr

組み込み関数:setattr

動かない

上のように書き直すと、動きません。
無限ループに陥って、止まります。*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にも、同様の欠点が挙げられています。

どうするのか

長くなるので、それはまた次回に…
こういう場合は、メタクラスを使うとよい、と紹介されています。

前回、名前を渡すの止めるべき、ということを書きましたが、次回に回します。


*1:詳細な説明は行ってこなかったが、ここでは、getsetを両方とも持つ、「データディスクリプタ」である場合をいう

*2:なお、getattrの第三引数は、属性が見つからない場合に返す値

*3:例外が発生して、止まってくれる