検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>

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

Published 2020年7月27日21:34 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

次のプログラムを実行した際に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'



どうでしょうか?


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


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


そして デスクリプタが働くのは、自分自身のインスタンスが、あるクラスのクラス属性値として存在している場合のみ、ということです。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【 Effective Python, 2nd Edition 】__set_name__ デスクリプタ専用特殊関数 ( special method for descriptor ) を利用してデスクリプタインスタンスを割り当てたクラス変数名を取得し、コードの冗長性を排除しよう!
【 Effective Python, 2nd Edition 】デスクリプタ ( descriptor ) を利用して @property で行っていた属性値への操作を再利用できるようにしよう!
【Python 雑談・雑学】 デコレータ (decorators) を理解しよう - デコレータ、オリジナル関数からの引数の渡し方、受け取り方
【 Effective Python, 2nd Edition 】__getattr__、__getattribute__、__setattr__ メソッドを利用して、事前に定義していないインスタンス属性を操作しよう!
【Python 雑談・雑学 + coding challenge】シーケンス ( sequence ) における インデックス ( index ) を使った要素 1 つの取り出しと、スライス ( slice ) を利用した場合の取り出しの違いをちゃんと理解していますか?
【Python 雑談・雑学 + coding challenge】iterator protocol の実装 --- __iter__ 特殊関数は何を返すべき? イテレータオブジェクト ( iterator object ) なら何でも、そう、generator expression でもOKです!
【Python 雑談・雑学 + coding challenge】sorted 組み込み関数の key パラメータをうまく使って、カスタムオブジェクトを簡単にソートしよう! __getitem__、__len__ 特殊関数 ( special methods, dunder methods ) を実装すれば立派なシーケンス ( sequence ) です