1. tag1216

    Posted

    tag1216
Changes in title
+デスクリプタ(descriptor)で属性へのアクセスロジックを共通化
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,192 @@
+# はじめに
+
+クラスを定義する際に、複数の属性に対してバリデーションなどの共通ロジックを使いたいという場合があります。
+
+こういう時はデスクリプタ(descriptor)を使うと、同じコードを何度も書くことなく共通ロジックを再利用することができます。
+
+# デスクリプタ(descriptor)とは
+
+デスクリプタは属性へのアクセスロジックを実装したクラスです。
+
+デスクリプタには`__get__`、`__set__`、`__delete__`のメソッド全て又は一部を実装します。
+
+- `def __get__(self, instance, owner):`
+ - getterに相当するメソッドで、`instance`から属性値を取得する際に呼ばれます。
+- `def __set__(self, instance, value):`
+ - setterに相当するメソッドで、`instance`に属性値を設定する際に呼ばれます。
+- `def __delete__(self, instance):`
+ - `instance`から属性値を削除する際に呼ばれます。
+
+3.6からは以下のメソッドも使えます。
+
+- `def __set_name__(self, owner, name):`
+ - デスクリプタをクラス属性に設定した時に属性名が渡されます。
+
+# デスクリプタの簡単な例
+
+デスクリプタの動作がわかるように、各メソッドに`print()`を入れただけの単純なデスクリプタを作成してみます。
+
+```py3:デスクリプタ
+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
+```
+
+このデスクリプタを使用するには、以下のようにクラス属性としてデスクリプタのインスタンスを作成します。
+
+```py3:デスクリプタを使用するクラス
+class MyClass:
+ a = TraceDescriptor()
+ b = TraceDescriptor()
+```
+
+上記クラスのインスタンスを作成して属性にアクセスすると、デスクリプタのメソッドが呼び出されます。
+
+```py3
+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
+```
+
+# 型変換を行うデスクリプタ
+
+もう少し実践的な例を考えてみます。
+
+テキストファイルなどからデータを読み込んでオブジェクトにセットするときに、いちいち文字列を特定の型に変換するのは面倒です。
+
+そんな時のために、値を設定する際に指定の型に変換するデスクリプタを作ってみます。
+
+```py3
+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__`でその型に変換を行います。
+
+```py3
+class MyClass:
+ str_value = TypedField(str)
+ int_value = TypedField(int)
+```
+
+これで値を設定する時に自動的に型を変換してくれます。
+
+```pycon
+>>> 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`が発生します。
+
+```pycon
+>>> 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'
+```
+
+# デスクリプタを使わない場合
+
+参考までにデスクリプタを使わないで書くとこうなります。
+
+```py3
+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 ガイド](https://docs.python.jp/3/howto/descriptor.html) には、以下のように書かれています。
+
+> デスクリプタについて学ぶことにより、新しいツールセットが使えるようになるだけでなく、Python の仕組みや、洗練された設計のアプリケーションについてのより深い理解が得られます。
+
+デスクリプタはプロパティ(`@property`)やメソッド(インスタンスメソッド、`@staticmethod`、`@classmethod`)などPythonのクラス定義のコアな部分に使用されている他、Djangoのモデルなどフレームワークの実装でも活用されています。
+
+デスクリプタを使いこなせるかどうかが、Python上級者になるためのひとつの境目のような気がしてきました。
+
+# 参考
+
+- Pythonドキュメント
+ - 言語リファレンス
+ - 3. データモデル
+ - [3.3.2.1. デスクリプタ (descriptor) の実装](https://docs.python.jp/3/reference/datamodel.html#implementing-descriptors)
+ - Python HOWTO
+ - [デスクリプタ HowTo ガイド](https://docs.python.jp/3/howto/descriptor.html)
+- Qiita
+ - [ディスクリプタを制する者は Python を制す](https://qiita.com/koshigoe/items/848ddc0272b3cee92134#%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%82%BF)
+ - [Python を支える技術 ディスクリプタ編 #pyconjp](https://qiita.com/knzm/items/a8a0fead6e1706663c22)
+