SE(たぶん)の雑感記

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

Pythonのディスクリプタについて:2.内容説明

前回の記事では、ディスクリプタと言いながら、プロパティとフィールドについて説明しました。

hiroronn.hatenablog.jp

参考書籍

前回と同じですが、載せておきます。

honto.jp

前回要約

  • Pythonのプロパティは、@property@プロパティ名.setterで表現する
  • Pythonのフィールドは、インスタンスに紐づけるものと、クラスに紐づけるものがある

例:プロパティに追加実装

前回記事時点のExamクラス

前回記事時点では、

class Exam(object):
    """試験結果"""

    def __init__(self):
        """コンストラクタ"""
        self._math_grade = 0
        self._writing_grade = 0
        self._science_grade = 0

    @property
    def math_grade(self):
        """数学"""
        return self._math_grade

    @math_grade.setter
    def math_grade(self, value):
        self._math_grade = value

    # 以下省略

というようなクラスを作成しました。

修正内容

Examクラスの、各grade(点数)について、「0点から100点までの値のみ許容する」ものとします。

そうすると、math_gradeのセッターを例にすると

@math_grade.setter
def math_grade(self, value):
    if not (0 <= value <= 100):
        raise ValueError("Grade must be between 0 and 100")
    self._math_grade = value

# 以下省略

のように、セット前に

if not (0 <= value <= 100):
    raise ValueError("Grade must be between 0 and 100")

というチェックを加える必要があります。*1

問題点

前回出した、Examクラスには、gradeプロパティが3つありました。
それぞれのプロパティのセッターで、同じチェックを組み込むのは、面倒です。
また、チェックをメソッドにしたとしても、同じ記述を繰り返すため、やはり面倒です。*2

解決策:ディスクリプタ

Effective Pythonでは、こういう同じようなプロパティを何度も書くという場合、ディスクリプタの使用を勧めています。

ディスクリプタとは

分かりやすく言うと、定型のゲッターとセッターを持ったプロパティとして機能するクラスです。*3

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

Examクラスのmath_gradeにアクセスする際、

exam.math_grade = 60

のような書き方をします。
前回の記事では、math__gradeはプロパティでしたが、同様のことがディスクリプタを使っても実現できます。

宣言

宣言は、以下のように行います。
なお、Exammath_grade等のプロパティ名に従い、Gradeクラスとします。

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 instance.__dict__[self.name]

    def __set__(self, instance, value: int):
        """setter descriptor"""
        if not (0 <= value <= 100):
            raise ValueError("Grade must be between 0 and 100")
        instance.__dict__[self.name] = value
from grade import Grade

class Exam(object):
    """試験結果"""

    math_grade = Grade("math_grade")
    writing_grade = Grade("writing_grade")
    science_grade = Grade("science_grade")

    def total(self):
        """合計点数"""
        return self.math_grade + self.writing_grade + self.science_grade
    # 宣言していたプロパティは、全て削除

Examクラスのソースは、上のものだけになります。非常にすっきりします。

使い方

  • 宣言

__get____set____delete__いずれかのメソッドを定義すると、ディスクリプタクラスとして扱われます。

プロパティとして使うなら、__get____set__を定義します。それぞれ、プロパティのゲッター、セッターと対応します。

ディスクリプタクラスのインスタンスを、クラス属性にセットします。
__init__内で、self.math_gradeのように(インスタンス属性として)宣言しても、ディスクリプタとしては扱われません。

  • クラス利用側 前と変わらず、
exam = Exam()
exam.math_grade = 60
exam.writing_grade = 80
exam.science_grade = 90
print(exam.total) # 230

で良いです。

なお、前回同様、Exam.__dict__でクラス辞書を出力すると、

{
…省略
'math_grade': <grade.Grade object at 0x000001B1622EB710>, 
'writing_grade': <grade.Grade object at 0x000001B1622EB7F0>, 
'science_grade': <grade.Grade object at 0x000001B1622EB898>, 
…
}

のように、Gradeオブジェクトとして扱われています。

知っておくべきこと

Examのクラス属性にあるGradeオブジェクトですが、前回説明したうちのmutableなクラス変数となります。
よって、Gradeインスタンスは、Examの全インスタンスで共有となります。

どのExamインスタンスからアクセスされたかについては、引数instanceで判断できます。
__get____set__では、instanceにアクセスすることで、プロパティのような動作を実現しています。

まとめ

分かりづらかったらすみません。

(大事だけど)余談

ディスクリプタですが、本来なら、クラスやインスタンスは、辞書形式で属性を管理している、等の説明を入れる必要がありますが、それだけで記事が書けてしまう程度には脱線するので、触れていません。ご了承ください。

また、ディスクリプタは、Pythonの多くの部分で使われており、プロパティも実際はディスクリプタです。

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

また、クラス内の関数も、ディスクリプタで実現されています。Pythonすっきりしていてすごい。

続き

ディスクリプタクラス内で、

instance.__dict__[self.name]

のようにして、インスタンスの属性にアクセスしています。
この辺りの説明をする必要があると思うので、次回はその話を書きます。

また、インスタンスの属性アクセスには、getattrsetattrという標準メソッドがあり、それを使うべきですが、私が試した順番で記述しているので、次の記事で、そのメソッドを使うとどうなるか、について書きます。

あと、Gradeクラスのコンストラクタで、文字列で名前を受け取っていますが、これも面倒なので止めるべきです。
そういうことも、次回の記事で書きます。

2018/1/18追記:記事書きました!

hiroronn.hatenablog.jp


*1:Pythonでは、「0 <= value <= 100」というような書き方で、変数の範囲チェックが可能。C#等、同じ書き方ができない言語は多い。

*2:面倒な上に、全てのチェック条件を修正する際、プロパティの数だけ修正が必要で、保守性も良くない

*3:正確には、ゲッターだけでも良いし、プロパティとして使う必要もない

*4:本では、コンストラクタで名前を渡さないが、混乱の元と感じたので、名前を渡している