Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
5
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

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

この記事は 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上級者になるためのひとつの境目のような気がしてきました。

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
5
Help us understand the problem. What are the problem?