描述器
May 11, 2022能被稱為描述器(Descriptor)的物件,必須擁有 __get__
方法,以及選擇性的 __set__
、__delete__
方法,這三個方法的簽署如下:
def __get__(self, instance, instance_type)
def __set__(self, instance, value)
def __delete__(self, instance)
描述器的運作
Python 的描述器,是用來描述屬性的取得、設定、刪除該如何處理的物件,當描述器成為某類別的屬性成員,對於類別屬性或者其實例屬性的取得、設定或刪除,會交由描述器來決定處理方式(除了那些內建屬性,如 __class__
等屬性之外)。例如:
class Descriptor:
def __get__(self, instance, instance_type):
print(self, instance, instance_type, end = '\n\n')
def __set__(self, instance, value):
print(self, instance, value, end = '\n\n')
def __delete__(self, instance):
print(self, instance, end = '\n\n')
class Some:
x = Descriptor()
s = Some()
s.x
s.x = 10
del s.x
Some.x
在參數的部份,instance
是描述器所在類別的實例,instance_type
則是描述器所在類別(有時也會命名為 owner
),Descriptor
指定給 Some
類別的 x
屬性時,對於 Some
實例 s
的屬性取值、指定或刪除,分別相當於進行以下的動作:
Some.__dict__['x'].__get__(s, Some)
Some.__dict__['x'].__set__(s, 10)
Some.__dict__['x'].__delete__(s)
對於 Some.x
這個取值動作,相當於:
Some.__dict__['x'].__get__(None, Some)
因此,上面這個範例的執行結果會是:
<__main__.Descriptor object at 0x0000021CA9A01FD0> <__main__.Some object at 0x0000021CA9A01FA0> <class '__main__.Some'>
<__main__.Descriptor object at 0x0000021CA9A01FD0> <__main__.Some object at 0x0000021CA9A01FA0> 10
<__main__.Descriptor object at 0x0000021CA9A01FD0> <__main__.Some object at 0x0000021CA9A01FA0>
<__main__.Descriptor object at 0x0000021CA9A01FD0> None <class '__main__.Some'>
這就是描述器與〈__getattribute__、__getattr__、__setattr__、__delattr__〉談到的那些特殊方法不同之處,描述器可以取得更多的資訊,而且描述器本身是個物件,本身被存取時呼叫對應的協定,而不是存取其所在類別實例的各個屬性時被呼叫。
描述器的最基本協定是具備 __get__
方法,若還具有 __set__
或 __delete__
方法或兩者兼具,可稱為資料描述器(Data descriptor)。
僅有 __get__
方法的描述器,稱為非資料描述器(Non-data descriptor)。對於非資料描述器,若實例上有對應的屬性,描述器就不會有動作。例如:
>>> class Desc:
... def __get__(self, instance, instance_type):
... print('instance', instance, 'instance_type', instance_type)
...
>>> class X:
... x = Desc()
...
>>> x = X()
>>> x.x
instance <__main__.X object at 0x000002708FEDED30> instance_type <class '__main__.X'>
>>> x.x = 10
>>> x.x
10
>>> del x.x
>>> x.x
instance <__main__.X object at 0x000002708FEDED30> instance_type <class '__main__.X'>
>>>
在上面的示範中,一旦 X
的實例被指定 x
屬性值,就看不到描述器有動作了。簡單來說,資料描述器可以攔截對實例的屬性取得、設定與刪除行為;非資料描述器,是用來攔截透過實例取得類別屬性時的行為。
模擬 @property
在〈屬性與方法〉看過 @property
,是用來將對實例的屬性存取,轉為呼叫 @property
標註之函式,可想而知的,這是一種資料描述器的行為,這邊來自行模擬類似的功能:
class PropDescriptor:
def __init__(self, getter, setter):
self.getter = getter
self.setter = setter
def __get__(self, instance, instance_type):
return self.getter(instance)
def __set__(self, instance, value):
self.setter(instance, value)
def prop(getter, setter):
return PropDescriptor(getter, setter)
class Ball:
def __init__(self, radius):
if radius <= 0:
raise ValueError('必須是正數')
self.__radius = radius
def get_radius(self):
return self.__radius
def set_radius(self, radius):
self.__radius = radius
radius = prop(get_radius, set_radius)
ball = Ball(10)
print(ball.radius) # 顯示 10
ball.radius = 5
print(ball.radius) # 顯示 5
將 get_radius
、set_radius
傳入 prop
,它會傳回一個描述器,這個描述器被指定為 Ball
類別的 radius
,因此,對於實例的 radius
屬性存取,都會透過描述器處理,也就是呼叫傳入的 get_radius
、set_radius
方法來處理。
__set_name__ 方法
Python 3.6 以後,描述器可以定義 __set__name__
方法,這個方法會在其所在類別定義完成後執行,傳入描述所在類別及參考描述器的名稱,這就讓你有機會額外地對類別或名稱做些處理,例如,若要達成方才範例的功能,也可以採用另一種方式實作:
class PropDescriptor:
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, instance, instance_type):
return getattr(instance, self.private_name)
def __set__(self, instance, value):
setattr(instance, self.private_name, value)
class Ball:
radius = PropDescriptor()
def __init__(self, radius):
if radius <= 0:
raise ValueError('必須是正數')
self.radius = radius
ball = Ball(10)
print(ball.radius) # 顯示 10
ball.radius = 5
print(ball.radius) # 顯示 5