LoginSignup
9
5

More than 5 years have passed since last update.

デスクリプタ(descriptor)で属性へのアクセスロジックを共通化

Last updated at Posted at 2017-12-25

この記事は Pythonのコードを短く簡潔に書くテクニック Advent Calendar 2017 の25日目です。

はじめに

クラスを定義する際に、複数の属性に対してバリデーションなどの共通ロジックを使いたいという場合があります。

こういう時はデスクリプタ(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()を入れただけの単純なデスクリプタを作成してみます。

デスクリプタ
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上級者になるためのひとつの境目のような気がしてきました。

参考

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5