【Python 雑談・雑学】デスクリプタ ( descriptor ) をしっかりと理解しよう! 投稿一覧へ戻る

Tags: Python , miscellaneous , descriptor , __set__ , __get__

Published 2020年7月27日21:34 by T.Tsuyoshi

次のプログラムを実行した際に4つの print 文で出力されるそれぞれの内容が分かりますか?

class Field:
def __init__(self, name):
self.name = name
self.internal_name = '_' + self.name

def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.internal_name, '')

def __set__(self, instance, value):
setattr(instance, self.internal_name, value)


class Customer:
first_name = Field('first_name')


cur = Customer()


print(f"Before(cur.__dict__): {cur.__dict__!r}") # 1:
print(f"Before(cur.first_name): {cur.first_name!r}") # 2:


cur.first_name = 'Nana'


print(f"After(cur.__dict__): {cur.__dict__!r}") # 3:
print(f"After(cur.first_name): {cur.first_name!r}") # 4:



正解は次の通りです。


1: Before(cur.__dict__): {}

2: Before(cur.first_name): ''

3: After(cur.__dict__): {'_first_name': 'Nana'}

4: After(cur.first_name): 'Nana'



もし何の迷いもなく当然のように正解した方はこの記事を読む必要はまったくありません。


迷った挙句答えを間違ってしまった方は、多分デスクリプタ ( descriptor ) の作用機序の理解が曖昧なのだと思います。


今回の記事の目的は「デスクリプタの働きをちゃんと理解する」です。


ではプログラム構成部品を1つ1つ見ていきましょう。


class Field:
"""
パラメータの先頭に '_' を付加して protected 属性名を作成し、
その protected 属性名による属性アクセス機能を提供する。
"""

def __init__(self, name: str):
self.name = name
self.internal_name = '_' + self.name

def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.internal_name, '')

def __set__(self, instance, value):
setattr(instance, self.internal_name, value)



これがデスクリプタクラスです。なぜ?


__get__、__set__、もしくは __del__ 特殊関数のいずれかが実装されているからです。これら3つの特殊関数は特にデスクリプタプロトコルメソッド ( descriptor protocol methods ) と呼ばれます。


そして これらの関数は、デスクリプタクラスのインスタンスが、あるクラス ( これがオーナークラス; owner class ) の属性の値として存在する場合のみ機能 します。


今回の例で言えば、Customer クラスがそれに当たります。


class Customer:
first_name = Field('first_name')



Customer クラスには first_name クラス属性が定義されていて、その値は Field デスクリプタクラスのインスタンスです。


ですから、Customer クラスのインスタンスからこの属性 ( first_name ) に対するアクセスは、Field デスクリプタインスタンスの __get__ もしくは __set__ 特殊関数を呼び出す、ということになります。


ここまでの関係をまずしっかり理解してください。


理解を確実なものにするために、この時点までの Customer クラス、Customer クラスのクラス属性 first_name の値である Field('first_name') デスクリプタインスタンス、Field('first_name') オブジェクトの元である Field デスクリプタクラス、それぞれの属性辞書を確認しましょう。


print(f"{Customer.__dict__!r}")

# mappingproxy({..., 'first_name': <__main__.Field object at 0x0000000001E07430>, ...})


print(f"{Customer.__dict__['first_name'].__dict__!r}")

# {'name': 'first_name', 'internal_name': '_first_name'}


print(f"{Customer.__dict__['first_name'].__class__.__dict__!r}")

# mappingproxy({..., '__init__': <function Field.__init__ at 0x0000000002457790>, '__get__': <function Field.__get__ at 0x0000000002457DC0>, '__set__': <function Field.__set__ at 0x0000000002457A60>, ...)



さてプログラムでは続いて Customer クラスのインスタンス cur を作成しています。


cur = Customer()



さぁ、ここで1つ目の print 文が登場します。


print(f"Before(cur.__dict__): {cur.__dict__!r}") # 1:



ここでは Customer クラスインスタンスである cur オブジェクトの属性辞書を出力しています。


# 1: Before(cur.__dict__): {}



空の辞書です。大丈夫ですよね?


first_name 属性は Customer クラスのクラス属性です。ですからインスタンス属性辞書には含まれていません。


さて2つ目の print 文です。


print(f"Before(cur.first_name): {cur.first_name!r}") # 2:



ここでやっていることは、Customer クラスインスタンス cur オブジェクトの first_name 属性の値を取得することです。


そこでまず Python は、cur.__dict__ に 'first_name' という属性名が含まれているかを検索します。ありませんでしたよね。


続いて Python は、type(cur).__dict__ に 'first_name' という属性名が含まれているかを検索します。


ここで type(cur) は cur オブジェクトの「型」ですから、type(cur) -> Customer クラス、ということです。


print(type(cur))

# <class '__main__.Customer'>



Customer.__dict__ には 'first_name' という属性名が含まれていました。


そしてその値は、Field('first_name') というメソッドインスタンスでした。


Python はそのメソッドインスタンスを走査します。


そして Python は発見しちゃうんです、おっ、このオブジェクトは Field クラスのインスタンスで、Field クラスには __get__ と __set__ 特殊関数が定義されているって。


ということは、Field クラスはデスクリプタで、そのインスタンスがクラス ( Customer ) 属性の値として存在しているのだから、cur.first_name の値は通常通り属性辞書から取り出すのではなくて、その処理は Field デスクリプタの __get__ 特殊関数に任せなきゃ、ということになるわけです。


つまり、cur.first_name という属性値を取得するための式は、


type(cur).__dict__['first_name'].__get__(cur, type(cur))



と解釈されて処理される、というわけです。


そこで、Field('first_name').__get__(cur, Customer) の処理を追いかけてみます。


def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.internal_name, '')



すると、getattr(instance, self.internale_name, '') の結果が返ってきていることが分かります。


つまり、cur インスタンスの属性辞書の中で self.internal_name -> '_first_name' というキーがあればその値を、なければ '' (空の文字列) を返してるんですね。


そして、cur インスタンス属性辞書は空でしたから '_first_name' というキーが存在するはずもなく、結果として '' が返されてきて表示された、ということです。


print(f"Before(cur.first_name): {cur.first_name!r}") # 2:

# 2: Before(cur.first_name): ''



続いてプログラムでは属性に対して代入が行われています。


cur.first_name = 'Nana'



これも考え方は一緒です。


cur.first_name は結局 Customer.__dict__['first_name'] で、その値は Field('first_name') というデスクリプタインスタンスです。


デスクリプタインスタンスがクラス属性の値として存在していますから、この代入文は、「属性辞書の first_name というキーに 'Nana' という値をセットする」という通常の動作ではなくて、Field デスクリプタの __set__ 特殊関数に処理を任せることになります。


つまり、


type(cur).__dict__['first_name'].__set__(cur, 'Nana')



と解釈されて処理されるわけです。


Field デスクリプタクラスの __set__ 特殊関数をもう一度見てみましょう。


def __set__(self, instance, value):
setattr(instance, self.internal_name, value)



ここでは、cur インスタンスオブジェクトの属性辞書の self.internal_name -> '_first_name' というキーに 'Nana' という値を書き込んでいます。


そして今回は '_first_name' というキーは存在しませんから新規に追加されています。


ですから、


print(f"After(cur.__dict__): {cur.__dict__!r}") # 3:



の出力は


# 3: After(cur.__dict__): {'_first_name': 'Nana'}



となるわけです。


そして次の print 文は 2: と同じ流れですが、今度は cur インスタンスオブジェクトの属性辞書には '_first_name' というキーが存在しますからその値が返ってきています。


print(f"After(cur.first_name): {cur.first_name!r}") # 4:

# 4: After(cur.first_name): 'Nana'



どうでしょうか?


何となくデスクリプタの働きが分かったでしょうか?


この例のように、属性辞書からの値の取り出し、属性辞書への値の書き込み、という属性への通常のアクセス方法をオーバーライドするのがデスクリプタの役割 です。


そして デスクリプタが働くのは、自分自身のインスタンスが、あるクラスのクラス属性値として存在している場合のみ、ということです。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)