この記事は Pythonのコードを短く簡潔に書くテクニック Advent Calendar 2017 の25日目です。
はじめに
クラスを定義する際に、複数の属性に対してバリデーションなどの共通ロジックを使いたいという場合があります。
こういう時はデスクリプタ(descriptor)を使うと、同じコードを何度も書くことなく共通ロジックを再利用することができます。
デスクリプタ(descriptor)とは
デスクリプタは属性へのアクセスロジックを実装したクラスです。
デスクリプタには__get__
、__set__
、__delete__
のメソッド全て又は一部を実装します。
-
def __get__(self, instance, owner):
- getterに相当するメソッドで、
instance
から属性値を取得する際に呼ばれます。
- getterに相当するメソッドで、
-
def __set__(self, instance, value):
- setterに相当するメソッドで、
instance
に属性値を設定する際に呼ばれます。
- setterに相当するメソッドで、
-
def __delete__(self, instance):
-
instance
から属性値を削除する際に呼ばれます。
-
3.6からは以下のメソッドも使えます。
-
def __set_name__(self, owner, name):
- デスクリプタをクラス属性に設定した時に属性名が渡されます。
デスクリプタの簡単な例
デスクリプタの動作がわかるように、各メソッドにprint()
を入れただけの単純なデスクリプタを作成してみます。
class TraceDescriptor:
def __set_name__(self, owner, name):
print(f'__set_name__: {name}')
self.name = name
def __get__(self, instance, owner):
value = instance.__dict__[self.name]
print(f'__get__: {self.name} {value}')
return value
def __set__(self, instance, value):
print(f'__set__: {self.name} {value}')
instance.__dict__[self.name] = value
このデスクリプタを使用するには、以下のようにクラス属性としてデスクリプタのインスタンスを作成します。
class MyClass:
a = TraceDescriptor()
b = TraceDescriptor()
上記クラスのインスタンスを作成して属性にアクセスすると、デスクリプタのメソッドが呼び出されます。
instance = MyClass()
instance.a = 123
print(instance.a)
instance.b = 'abc'
print(instance.b)
__set_name__: a
__set_name__: b
__set__: a 123
__get__: a 123
123
__set__: b abc
__get__: b abc
abc
1 2
型変換を行うデスクリプタ
もう少し実践的な例を考えてみます。
テキストファイルなどからデータを読み込んでオブジェクトにセットするときに、いちいち文字列を特定の型に変換するのは面倒です。
そんな時のために、値を設定する際に指定の型に変換するデスクリプタを作ってみます。
class TypedField:
def __init__(self, field_type):
self.field_type = field_type
def __set_name__(self, owner, name):
self.name = name
def __set__(self, instance, value):
if not isinstance(value, self.field_type):
value = self.field_type(value)
instance.__dict__[self.name] = value
デスクリプタのコンストラクタで型を受け取るようにして、__set__
でその型に変換を行います。
class MyClass:
str_value = TypedField(str)
int_value = TypedField(int)
これで値を設定する時に自動的に型を変換してくれます。
>>> instance = MyClass()
>>>
>>> instance.str_value = 123
>>> instance.str_value
'123'
>>> type(instance.str_value)
<class 'str'>
>>>
>>> instance.int_value = '123'
>>> instance.int_value
123
>>> type(instance.int_value)
<class 'int'>
当然ですが型変換ができない時にはValueError
が発生します。
>>> instance.int_value = 'abc'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in __set__
ValueError: invalid literal for int() with base 10: 'abc'
デスクリプタを使わない場合
参考までにデスクリプタを使わないで書くとこうなります。
def convert(value, value_type):
if not isinstance(value, value_type):
value = value_type(value)
return value
class MyClass:
@property
def str_value(self):
return self.__dict__['str_value']
@str_value.setter
def str_value(self, value):
value = convert(value, int)
self.__dict__['str_value'] = value
@property
def int_value(self):
return self.__dict__['int_value']
@int_value.setter
def int_value(self, value):
value = convert(value, int)
self.__dict__['int_value'] = value
属性一つ定義するのにgetterとsetterを実装しなければならず、属性が増えるたびに同じようなコードを書かなくてはなりません。
最後に
Pythonドキュメントの デスクリプタ HowTo ガイド には、以下のように書かれています。
デスクリプタについて学ぶことにより、新しいツールセットが使えるようになるだけでなく、Python の仕組みや、洗練された設計のアプリケーションについてのより深い理解が得られます。
デスクリプタはプロパティ(@property
)やメソッド(インスタンスメソッド、@staticmethod
、@classmethod
)などPythonのクラス定義のコアな部分に使用されている他、Djangoのモデルなどフレームワークの実装でも活用されています。
デスクリプタを使いこなせるかどうかが、Python上級者になるためのひとつの境目のような気がしてきました。
参考
- Pythonドキュメント
- 言語リファレンス
- Python HOWTO
- Qiita