前回の記事では、ディスクリプタと言いながら、プロパティとフィールドについて説明しました。
参考書籍
前回と同じですが、載せておきます。
前回要約
例:プロパティに追加実装
前回記事時点の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
はプロパティでしたが、同様のことがディスクリプタを使っても実現できます。
宣言
宣言は、以下のように行います。
なお、Exam
のmath_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には、同じような定義のプロパティをクラスに切り出せる、ディスクリプタという機能がある
- ディスクリプタを定義する場合、
__get__
等のメソッドを定義する - ディスクリプタをプロパティのように利用する場合、クラス属性として、インスタンスを登録する
- 属性へのアクセスは、プロパティと同じようにできる
分かりづらかったらすみません。
(大事だけど)余談
ディスクリプタですが、本来なら、クラスやインスタンスは、辞書形式で属性を管理している、等の説明を入れる必要がありますが、それだけで記事が書けてしまう程度には脱線するので、触れていません。ご了承ください。
また、ディスクリプタは、Pythonの多くの部分で使われており、プロパティも実際はディスクリプタです。
デスクリプタ HowTo ガイド — Python 3.6.3 ドキュメント
また、クラス内の関数も、ディスクリプタで実現されています。Pythonすっきりしていてすごい。
続き
ディスクリプタクラス内で、
instance.__dict__[self.name]
のようにして、インスタンスの属性にアクセスしています。
この辺りの説明をする必要があると思うので、次回はその話を書きます。
また、インスタンスの属性アクセスには、getattr
やsetattr
という標準メソッドがあり、それを使うべきですが、私が試した順番で記述しているので、次の記事で、そのメソッドを使うとどうなるか、について書きます。
あと、Grade
クラスのコンストラクタで、文字列で名前を受け取っていますが、これも面倒なので止めるべきです。
そういうことも、次回の記事で書きます。
2018/1/18追記:記事書きました!