property
內部實作了descriptor protocol
,所以可以視其為一種簡易版,單次使用的data descriptor
。在我們需要少量descriptor
特性時,property
是一個非常方便的工具。
今天假設在剛開始實作一個class
時,我們使用了property
快速完成了一個prototype。當我們準備進一步往下,繼續實作更多細節的時候,發現使用了很多具有相同邏輯的property
,所以決定自己實作一個descriptor
,將這些邏輯包起來重複利用。
我們會練習三個小題,練習1
是概念說明,練習2
及練習3
是實戰例題。我們將使用方法6
來實作,方便大家之後可以對照Descriptor HowTo Guide中的講解,因這兩個小題皆是受其啟發而來。
# 01a
中我們定義了三個prop
,他們的功能都是在返回其底層的self._x
、self._y
及self._y
。
# 01a
class MyClass:
def __init__(self, x, y, z):
self._x = x
self._y = y
self._z = z
@property
def x(self):
return self._x
@property
def y(self):
return self._y
@property
def z(self):
return self._z
if __name__ == '__main__':
my_inst = MyClass(1, 1, 1)
print(my_inst.x) # 1
print(my_inst.y) # 1
print(my_inst.z) # 1
# my_inst.x = 2 # ValueError
# my_inst.y = 2 # ValueError
# my_inst.z = 2 # ValueError
首先,MyClass.__init__
中的邏輯可以使用__set_name__
來達成。
# 01b
class myprop:
def __set_name__(self, owner_cls, name):
self._name = f'_{name}'
接著property
的getter
邏輯可以移至__get__
。
# 01b
class myprop:
...
def __get__(self, instance, owner_cls):
if instance is None:
return self
return getattr(instance, self._name)
乍看我們好像完成了改寫,我們的myprop
可以這麼使用。
# 01b
...
class MyClass:
x = myprop()
y = myprop()
z = myprop()
def __init__(self, x, y, z):
self._x = x
self._y = y
self._z = z
但是別忘了property
是實作了descriptor protocol
的data descriptor
,代表它不是只有getteer
,也有setter
及deleter
。當我們沒有給予property
setter
或deleter
時,如果對其進行set
或del
,會raise AttributeError
。
而我們的myprop
雖然可以正常get
,卻也能set
和del
而不raise AttributeError
,這不太符合我們希望的行為。事實上myprop
因為只有實作__get__
,所以是一個non-data descriptor
。
# 01b
...
if __name__ == '__main__':
my_inst = MyClass(1, 1, 1)
print(my_inst.x) # 1 from __get__
my_inst.x = 2
print(my_inst.__dict__) # {'_x': 1, '_y': 1, '_z': 1, 'x': 2}
print(my_inst.x) # 2 from my_inst.__dict__
當第一次呼叫my_inst.x
時,使用的是myprop.__get__
,但是當我們使用了my_inst.x = 2
,這就相當於直接在my_inst.__dict__
加入x
一樣。當第二次呼叫my_inst.x
時,因為instance.__dict__
會shadow
myprop.__get__
,所以我們變成從instance.__dict__
取值了。
解決辦法是於myprop
中實作__set__
和__delete__
,但卻直接raise AttributeError
。這是一個非常容易出錯的盲點,只要能夠了解這邊,相信就能輕鬆穿梭在property
和descriptor
之間。
__get__
的 descriptor
是non-data descriptor
,是有機會被instance.__dict__
所shadow
的。__get__
加上__set__
或__delete__
兩種中至少一種的descriptor
是data descriptor
,其必定會shadow
instance.__dict__
。即使我們只是於__set__
和__delete__
raise AttributeError
,它依然是data descriptor
。這就像我們只給property
getter
,但沒有指定setter
及deleter
,property
依然是一個具有data descriptor
行為的obj
。於# 01c
中我們實作了__set__
及__delete__
,並模仿property
的錯誤訊息,直接raise AttributeError
。另外,為了讓myprop
更像property
,我們可以於__init__
中選擇性接收doc
來作為說明文件。
# 01c
class myprop:
def __init__(self, doc=None):
self.__doc__ = doc
def __get__(self, instance, owner_cls):
if instance is None:
return self
return getattr(instance, self._name)
def __set__(self, instance, value):
name = self._name[1:]
cls_name = type(instance).__name__
raise AttributeError(
f"myprop '{name}' of '{cls_name}' object has no setter")
def __delete__(self, instance):
name = self._name[1:]
cls_name = type(instance).__name__
raise AttributeError(
f"myprop '{name}' of '{cls_name}' object has no deleter")
def __set_name__(self, owner_cls, name):
self._name = f'_{name}'
class MyClass:
x = myprop()
y = myprop()
z = myprop()
def __init__(self, x, y, z):
self._x = x
self._y = y
self._z = z
# 02a
中我們定義了三個prop
,他們的功能都是:
self._validate
驗證,確認為int
且大於0
時,才會設定給self._x
、self._y
及self._y
。如果沒有通過self.__validate
驗證,會rasie ValueError
self._x
、self._y
及self._y
。# 02a
class MyClass:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._validate(value)
self._x = value
@property
def y(self):
return self._y
@y.setter
def y(self, value):
self._validate(value)
self._y = value
@property
def z(self):
return self._z
@z.setter
def z(self, value):
self._validate(value)
self._z = value
def _validate(self, value):
if not isinstance(value, int) or value <= 0:
raise ValueError(f'{value} is not a positive integer.')
首先,根據題意,我們可以給descriptor
取一個適合的名字為PositiveInt
。PositiveInt
的__get__
和__set_name__
與練習1
相似。
# 02b
class PositiveInt:
def __get__(self, instance, owner_cls):
if instance is None:
return self
return getattr(instance, self._name)
def __set_name__(self, owner_cls, name):
self._name = f'_{name}'
PositiveInt
的__set__
及_validate
是本題練習的重點。我們將__set__
分成驗證與實際設定兩部份。
# 02b
class PositiveInt:
...
def __set__(self, instance, value):
self._validate(value)
setattr(instance, self._name, value)
def _validate(self, value):
if not isinstance(value, int) or value <= 0:
raise ValueError(f'{value} is not a positive integer.')
藉助PositiveInt
我們可以將# 02a
改為# 02b
,是不是清爽不少呢?
# 02b
...
class MyClass:
x = PositiveInt()
y = PositiveInt()
z = PositiveInt()
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
練習3
和練習2
非常像,差別是這次我們想要建立NegativeInt
,讓我們可以在class
中使用任意數量的PositiveInt
與NegativeInt
,如# 03a
。
# 03a PSEUDO CODE!!!
class MyClass:
x = PositiveInt()
y = PositiveInt()
z = NegativeInt()
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
由於NegativeInt
與PositiveInt
不同之處,只有_validate
function
而已。或許您會想不如讓NegativeInt
繼承PositiveInt
,然後覆寫_validate
function
。此舉雖然可行,但是不太合理,這麼一來就像是暗示NegativeInt
是一種PositiveInt
。
一個比較好的方法,是再往上一層建立一個BaseValidator
,可以讓PositiveInt
與NegativeInt
繼承。
剛好Python有內建的abc
模組來幫助我們完成這件事:
abc.ABC
不允許使用者直接使用BaseValidator
生成instance
,只能被其它Validator
繼承。如果直接使用BaseValidator
生成instance
會raise TypeError
。abc.abstractmethod
可以提醒我們,繼承了BaseValidator
的Validator
必定要實作自己的_validate
function
,否則會於生成Validator
的instance
時raise TypeError
。# 03b
from abc import ABC, abstractmethod
class BaseValidator(ABC):
def __get__(self, instance, owner_cls):
if instance is None:
return self
return getattr(instance, self._name)
def __set__(self, instance, value):
self._validate(value)
setattr(instance, self._name, value)
def __set_name__(self, owner_cls, name):
self._name = f'_{name}'
@abstractmethod
def _validate(self, value):
pass
繼承了BaseValidator
後,我們的PositiveInt
與NegativeInt
只要實作_validate function
就好,於MyClass
中的使用方法也不需改變。整體的程式碼是不是看起來更有架構也更專業了呢?
# 03b
...
class PositiveInt(BaseValidator):
def _validate(self, value):
if not isinstance(value, int) or value <= 0:
raise ValueError(f'{value} is not a positive integer.')
class NegativeInt(BaseValidator):
def _validate(self, value):
if not isinstance(value, int) or value >= 0:
raise ValueError(f'{value} is not a negative integer.')
class MyClass:
x = PositiveInt()
y = PositiveInt()
z = NegativeInt()
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
__delete__
當property
沒有實作deleter
卻使用了類似del my_inst.x
的指令,其報錯為AttributeError: property 'x' of 'MyClass' object has no deleter
。當descriptor
沒有實作__delete__
卻使用了類似del my_inst.x
的指令,其報錯為AttributeError: __delete__
。
我們於練習1
實作__delete__
是為了讓其報錯訊息更像property
,如果是平常使用的話,大部份情況是不需要實作__delete__
,這跟我們使用property
的習慣是很像的。而且即使我們在沒有實作__delete__
的情況下,又使用了del my_inst.x
的指令,其報錯指令AttributeError: __delete__
也是非常清楚,容易了解。