在經過前面數翼的洗禮後,我們具備了閱讀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_mro
find_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_getattribute
object_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_hook
getattr_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代替。