在經過前面數翼的洗禮後,我們具備了閱讀Descriptor HowTo Guide比較深入部份的知識了。
dot來get跟set attribute。由於attribute lookup是一個複雜的主題,我們建議大家多看看不同高手的描述方法,可能比較容易心領神會。
除了Descriptor HowTo Guide外,我們特別推薦Dr. Fred Baptiste的Python3:Deep Dive-Part4課程及Ionel Cristian Mărieș的部落格文章。
本翼筆記為嘗試交叉參考上述資料而做。由於本翼難度頗高,再加上有些自己試著修改的程式碼,如果有錯誤的話,還望諸位先進可以不吝斧正,相當感謝。
今天我們將分別理解四個層面的dot:
至於super().attr,我們建議直接參考Guido的tutorial,但可能需要一些Python2的基礎,才能體會老爹於20年前建置super()的邏輯。但是即使是在有Python2及descriptor與metaclasses的基本知識下,這篇tutorial還是相當難啃呀!期望未來有一天我們能靜下心來,像欣賞Descriptor HowTo Guide一樣,好好拜讀幾遍。
首先我們來看一段Descriptor HowTo Guide開頭的描述:
The Ten class is a descriptor whose
__get__()method always returns the constant 10:...In the a.y lookup, the dot operator finds a descriptor instance, recognized by its__get__method. Calling that method returns 10.
class Ten:
def __get__(self, obj, objtype=None):
return 10
class A:
x = 5
y = Ten()
可以看出Raymond明確地說明Ten是descriptor,而y是descriptor instance。雖然在大多數情況,我們會將Ten及y都稱作descriptor。
為了方便溝通,我們於# 01定義幾個名詞。
# 01
class NonDataDescriptor:
def __get__(self, instance, owner_cls):
...
class DataDescriptor:
def __get__(self, instance, owner_cls):
...
def __set__(self, instance, value):
...
class DummyClass:
a = 1
class MyClass(DummyClass):
non_data_desc = NonDataDescriptor()
data_desc = DataDescriptor()
if __name__ == '__main__':
print(MyClass.__mro__) # MyClass, DummyClass, object
my_inst = MyClass()
NonDataDescriptor為Non-data descriptor,而non_data_desc為其生成的instance。DataDescriptor是data descriptor,而data_desc為其生成的instance。obj.attr在生成obj的class或其MRO上任一class的__dict__內,我們以obj.attr in cls_mro代稱,並以base來代稱attr所在的class。例如,# 01中的my_inst.a,因為a在MyClass(type(my_inst))的MRO上的DummyClass.__dict__,我們會稱my_inst.a in cls_mro,而其base則為DummyClass。obj.attr不在生成obj的class或其MRO上任一class的__dict__內,我們以obj.attr not in cls_mro代稱,obj為class且如果obj.attr在obj的class或其MRO上任一class的__dict__內,我們以obj.attr in obj_cls_mro代稱,並以obj_base來代稱attr所在的class。例如,# 01中的MyClass.a,因為a在MyClass的MRO上的DummyClass.__dict__,我們會稱MyClass.a in obj_cls_mro,而其obj_base則為DummyClass。object.__getattribute__是Python取得attribute值的dunder method。
object.__getattribute__下面是object.__getattribute__於Python的實作。
find_name_in_mrofind_name_in_mro接受三個參數:
cls為生成obj的class。name為想尋找的attribute(str型態)。default為一預設值。針對cls.__mro__打一個迴圈,依序在MRO上每個class的__dict__尋找有沒有name這個attribute,如果找到就馬上返回。當迴圈結束還是沒找到的話,則返回default。
def find_name_in_mro(cls, name, default):
"Emulate _PyType_Lookup() in Objects/typeobject.c"
for base in cls.__mro__:
if name in vars(base):
return vars(base)[name]
return default
object_getattributeobject_getattribute接受兩個參數,obj及name。object()建立一個獨特的預設值null。obj_type為生成obj的class。cls_var為呼叫find_name_in_mro回傳的結果。注意,cls_var可以不是普通的class variable。也可以是data_desc或non_data_desc,因為他們也可以視為一種class variable,所以Raymond這樣命名。descr_get為試圖由生成cls_var的class內去取__get__(即測試cls_var的class是否為descriptor)。如果取不到的話則返回預設值null。cls_var是否為data_desc。判斷方法是確定descr_get不是null且生成cls_var的class中有__set__或是__delete__。如果符合的話,代表cls_var是data_desc,呼叫它的__get__來取值。可以使用descr_get(cls_var, obj, objtype)或是cls_var.__get__(obj, objtype)兩種語法。cls_var不是data_desc則進行第二個判斷。看看obj有沒有__dict__且name是否在__dict__中。如果是的話,代表cls_var是instance variable,使用vars(obj)['attr']取值。cls_var也不是instance variable則進行第三個判斷。如果descr_get不是null的話,代表cls_var是non_data_desc,呼叫它的__get__來取值。descr_get(cls_var, obj, objtype)或是cls_var.__get__(obj, objtype)兩種語法。cls_var也不是non_data_desc的話,則進行第四個判斷。如果cls_var不是null的話,代表其為class varaible,返回cls_var。cls_var是null的話,代表經過前面四次確認後,仍無法順利取值,raise AttributeError。def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
cls_var = find_name_in_mro(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__')
or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype) # data descriptor
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj)[name] # instance variable
if descr_get is not null:
return descr_get(cls_var, obj, objtype) # non-data descriptor
if cls_var is not null:
return cls_var # class variable
raise AttributeError(name)
object.__getattr__當obj.attr raise AttributeError後,Python會呼叫obj.__getattr__作為最後一個補救措施(預設行為是直接raise AttributeError)。如果還是有AttributeError不能處理的話,則raise AttributeError給使用者。
由於object.__getattribute__的實作內,並未呼叫object.__getattr__,而是由Python自動偵測到AttributeError時呼叫。所以當顯性呼叫obj.__getattribute__或是super().__getattribute__時,object.__getattr__並不會被呼叫。
getattr_hookgetattr_hook接受兩個參數,obj及name。obj.__getattribute__,當raise AttributeError時,查看生成obj的class是否有__getattr__。
reraise AttributeError。type(obj).__getattr__。這邊要檢查type(obj)是否有__getattr__的原因,可能是因為一般的instance是沒有__getattr__的。如果直接return obj.__getattr__(name),會引起另一個AttributeError,那麼就需要另一個try-catch來處理,語法更為複雜。
def getattr_hook(obj, name):
"Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
try:
return obj.__getattribute__(name)
except AttributeError:
if not hasattr(type(obj), '__getattr__'):
raise
return type(obj).__getattr__(obj, name) # __getattr__
我們試著將當obj.attr(obj is instance)的lookup流程寫下來。
obj.attr in cls_mro且obj.attr是data_desc,則使用data_desc.__get__(obj, type(obj))。obj.attr in vars(obj),則返回vars(obj)['attr']。obj.attr in cls_mro且obj.attr是non_data_desc,則使用non_data_desc.__get__(obj, type(obj))。obj.attr in cls_mro則為class variable,返回vars(base)['attr']。raise AttributeError(自動呼叫obj.__getattr__)。
流程整理格式1是根據Raymond的筆記整理的,Dr. Fred Baptiste則建議由obj是否在cls_mro來作為分支思考。
obj.attr in cls_mro:
obj.attr是data_desc,則使用data_desc.__get__(obj, type(obj))。obj.attr in vars(obj),則返回vars(obj)['attr']。obj.attr是non_data_desc,則使用non_data_desc.__get__(obj, type(obj))。class variable,返回vars(base)['attr']。obj.attr not in cls_mro:
obj.attr in vars(obj),則返回vars(obj)['attr']。obj.attr not in vars(obj),則raise AttributeError(自動呼叫obj.__getattr__)。
Descriptor HowTo Guide講到invocation from a class時,是這麼說的:
The logic for a dotted lookup such as A.x is in
type.__getattribute__(). The steps are similar to those forobject.__getattribute__()but the instance dictionary lookup is replaced by a search through the class’s method resolution order. If a descriptor is found, it is invoked withdesc.__get__(None, A).
儘管文件中沒有提供相對應的Python程式碼。但是我們可以根據上面描述,試著實作看看。
# 02 Experiment code, not verified
def find_name_in_mro(cls, name, default):
cls_mro = object.__getattribute__(cls, '__mro__')
for base in cls_mro:
base_dict = object.__getattribute__(base, '__dict__')
if name in base_dict:
return base_dict[name]
return default
def type_getattribute(obj, name):
...
_cls_var = find_name_in_mro(obj, name, null)
if _cls_var is not null:
if getattr(type(_cls_var), '__get__', null) is not null:
return _cls_var.__get__(None, obj) # descriptor(of any kind)
return _cls_var # class variable
...
由於此時的obj是class,所以我們需要將整個思考邏輯往上推一層。現在type_getattribute的判斷式是針對metaclasses的MRO,而我們的instance lookup也因為往上推一層,而變成了class MRO的lookup,這也是整段程式唯一需要修改的地方。
find_name_in_mro做兩次MRO的搜尋,一次是於metaclass中,一次是於class中。當在class中搜尋中,如果使用了obj.__mro__或obj.__dict__,會造成Recursion Error。所以我們這邊改使用object.__getattribute__,這麼一來,新的find_name_in_mro就能同時適用於兩個情況。_cls_var為class MRO的搜尋結果。如果_cls_var不是null的話,代表其位於class或其MRO上任一class的__dict__中。我們可以利用與前面相同的技巧,看看getattr(type(_cls_var), '__get__', null)的回傳是否為null。
null,就代表_cls_var為descriptor的instance,依照文件說明,可以返回_cls_var.__get__(None, obj)。由於我們只需要_cls_var的__get__,所以相當於要補抓任何型態的descriptor instance,包括data_desc與non_data_desc,null,就代表_cls_var為class variable,直接返回即可。我們使用type_getattribute來作為MyType的__getattribute__,並確認於不同情況下可以成功取值。
# 02 Experiment code, not verified
...
class MyType(type):
def __getattribute__(self, name):
# confirm only call once for each dot access
print(f'MyType.__getattribute__ is called for {name=}')
return type_getattribute(self, name)
class NonDataDescriptor:
def __get__(self, instance, owner_cls):
return 10
class DataDescriptor:
def __get__(self, instance, owner_cls):
return 20
def __set__(self, instance, value):
...
class MyType1(MyType):
z = DataDescriptor()
class MyType2(MyType1):
y = NonDataDescriptor()
class MyType3(MyType2):
x = 1
class DummyClass:
d = 'dummy'
class MyClass(DummyClass, metaclass=MyType3):
a = 100
b = NonDataDescriptor()
c = DataDescriptor()
if __name__ == '__main__':
print(f'{MyClass.x=}') # 1
print(f'{MyClass.y=}') # 10
print(f'{MyClass.z=}') # 20
print(f'{MyClass.a=}') # 100
print(f'{MyClass.b=}') # 10
print(f'{MyClass.c=}') # 20
print(f'{MyClass.d=}') # 'dummy'
另外,也確認了當同一個attr出現於不同地方,也會依照我們寫下的邏輯來取值。
# 02a中由於t為位於MyType中的data_desc,優先級別最高。
# 02a
...
class MyType(type):
t = DataDescriptor()
def __getattribute__(self, name):
return type_getattribute(self, name)
class MyClass(metaclass=MyType):
t = 't_in_myclass'
if __name__ == '__main__':
print(f'{MyClass.t=}') # 20
# 02b、# 02c與# 02d都因為MyType中的t不是data_desc,而優先從MyClass中取值。
# 02b
...
class MyType(type):
t = NonDataDescriptor()
def __getattribute__(self, name):
return type_getattribute(self, name)
class MyClass(metaclass=MyType):
t = 't'
if __name__ == '__main__':
print(f'{MyClass.t=}') # 't'
# 02c
...
class MyType(type):
t = NonDataDescriptor()
def __getattribute__(self, name):
return type_getattribute(self, name)
class MyClass(metaclass=MyType):
t = DataDescriptor()
if __name__ == '__main__':
print(f'{MyClass.t=}') # 20
# 02d
...
class MyType(type):
t = 't_in_mytype'
def __getattribute__(self, name):
return type_getattribute(self, name)
class MyClass(metaclass=MyType):
t = NonDataDescriptor()
if __name__ == '__main__':
print(f'{MyClass.t=}') # 10
我們試著將obj.attr(obj is class)的lookup流程寫下來。
obj.attr in cls_mro且obj.attr是data_desc,則使用data_desc.__get__(obj, type(obj))。obj.attr in obj_cls_mro:
obj.attr是desc_inst,則使用desc.__get__(obj, type(obj))。obj.attr不是desc_inst,則返回vars(obj_base)['attr']。obj.attr in cls_mro且obj.attr是non_data_desc,則使用non_data_desc.__get__(obj, type(obj))。obj.attr in cls_mro則為class variable,返回vars(base)['attr']。raise AttributeError(自動呼叫obj.__getattr__)。
obj.attr in cls_mro:
obj.attr是data_desc,則使用data_desc.__get__(obj, type(obj))。obj.attr in obj_cls_mro:
obj.attr是desc_inst,則使用desc.__get__(obj, type(obj))。obj.attr不是desc_inst,則返回vars(obj_base)['attr']。obj.attr是non_data_desc,則使用non_data_desc.__get__(obj, type(obj))。class variable,返回vars(base)['attr']。obj.attr not in cls_mro:
obj.attr in obj_cls_mro:
obj.attr是desc_inst,則使用desc.__get__(obj, type(obj))。obj.attr不是desc_inst,則返回vars(obj_base)['attr']。obj.attr not in obj_cls_mro則raise AttributeError(自動呼叫obj.__getattr__)。
object.__setattr__是Python設定attribute值的dunder method(註1)。
根據Dr. Fred Baptiste的說明,我們可以將obj.attr=value整理如下:
obj.attr in cls_mro且obj.attr是data_desc,則使用data_desc.__set__(obj, value)。obj.attr有__dict__,則使用obj.__dict__['attr']=value。obj.attr沒有__dict__,則raise AttributeError。
object.__setattr__根據上面的流程整理,我們可以試著於Python中實作object.__setattr__。
descr_get與descr_set都不是null的話,代表cls_var是data_desc,使用其__set__來設定attribute值。可以使用descr_set(cls_var, obj, value)或cls_var.__get__(obj, value)兩種語法。obj有沒有__dict__。如果有的話,使用obj.__dict__[name] = value新增或修改attribute。raise AttributeError。# 03 Experiment code, not verified
def object_setattr(obj, name, value):
null = object()
objtype = type(obj)
cls_var = find_name_in_mro(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
descr_set = getattr(type(cls_var), '__set__', null)
if descr_get is not null and descr_set is not null:
descr_set(cls_var, obj, value) # data descriptor
return
if hasattr(obj, '__dict__'):
obj.__dict__[name] = value # instance variable
return
raise AttributeError(name)
...
我們使用object_setattr來作為Character中的__setattr__,並確認可以成功存取instance variable及使用descriptor instance。
# 03 Experiment code, not verified
...
class DataDescriptor:
def __get__(self, instance, owner_cls):
print('__get__ called')
if instance is None:
return self
return instance.__dict__.get(self._name)
def __set__(self, instance, value):
print('__set__ called')
instance.__dict__[self._name] = value
def __set_name__(self, owner_cls, name):
self._name = name
class Character:
dog = DataDescriptor()
def __init__(self, name):
self.name = name
def __setattr__(self, name, value):
print(f'Character.__setattr__ is called for {name=}. {value=}')
object_setattr(self, name, value)
if __name__ == '__main__':
john_wick = Character('John Wick')
print(john_wick.name) # John Wick
john_wick.dog = 'Daisy' # __set__ called
print(john_wick.dog) # __get__ called, Daisy
print(john_wick.__dict__) # {'name': 'John Wick' 'dog': 'Daisy'}
本小節的程式碼純屬實驗性質,強烈不建議這麼寫。但通過這個實作,我們學習思考了很多,所以想要做個筆記,留下記錄。
至於obj.attr=value(obj is class)需要mutate obj.__dict__(mappingproxy),所以需要overwrite type.__setattr__。但是這麼做會於呼叫setattr時,造成Recursion Error,除非直接使用type.__setattr__,我們沒有想到適合的解決方法。
在看了這篇stackoverflow的討論及C code之後,了解自己暫時沒有能力於Python實作type.__setattr__。
既然無法mutate obj.__dict__ ,我們嘗試每次需要mutate時都重新生成一個class,於是有了# 04。於每次MyType.__setattr__被呼叫時,複製class的資訊並加上新設定的attribute,接著利用exec重新產生新的class來取代原先的class。
# 04 Experiment code, not verified
def find_name_in_mro(cls, name, default):
"Emulate _PyType_Lookup() in Objects/typeobject.c"
for base in cls.__mro__:
if name in vars(base):
return vars(base)[name]
return default
def type_setattr(obj, name, value):
"""Just exploring the magic of Python, don't do this..."""
if hasattr(obj, '__dict__'):
cls_name = obj.__name__
cls_bases = obj.__mro__
cls_dict = dict(vars(obj)) | {name: value}
_globals = globals() | {'cls_name': cls_name,
'cls_bases': cls_bases,
'cls_dict': cls_dict}
meta = type(obj).__name__
cls_body = f'{cls_name}={meta}(cls_name, cls_bases, cls_dict)'
exec(cls_body, _globals, globals())
else:
raise AttributeError(name)
class MyType(type):
def __setattr__(self, name, value):
print(f'MyType.__setattr__ is called for {name=}. {value=}')
type_setattr(self, name, value)
...
這樣的寫法的確可以設定class variable及descriptor。從id比較中可以看出,於每次設定attribute時,都會新生成一個MyClass。
# 04 Experiment code, not verified
...
class DataDescriptor:
def __get__(self, instance, owner_cls):
if instance is None:
return self
return instance.__dict__.get(self._name)
def __set__(self, instance, value):
instance.__dict__[self._name] = value
def __set_name__(self, owner_cls, name):
self._name = name
class MyClass(metaclass=MyType):
a = 'a'
if __name__ == '__main__':
orig_cls_id = id(MyClass)
print(MyClass.a) # a
MyClass.a = 'b'
print(MyClass.a) # b
assert orig_cls_id != id(MyClass)
MyClass.c = DataDescriptor()
my_inst = MyClass()
my_inst.c = 'c'
print(my_inst.c) # c
print(MyClass.c) # <__main__.DataDescriptor object at 0x0000014667AE6690>
註1:Python有object.__getattribute__與object.__getattr__ ,但只有object.__setattr__而沒有object__setattribute__。
註2:本日流程圖為ChatGPT及PlantUML協作繪製而成。為方便排版,所有的double underscore都使用single underscore代替。