今天讓我們繼續跟著大神的腳步,一起閱讀Descriptor HowTo Guide的Pure Python Equivalents,來看看如何用Python實作property
、function and bound method
, staticmethod
、classmethod
及__slots__
。
__init__
中接收fget
、fset
、fdel
及doc
四個選擇性給予的變數。如果沒有給doc
但是fget
內有的話,會取fget
的doc
作為doc
。所以當我們使用@Property
來裝飾一個function
時,其實就是指定該function
為Property
的第一個變數fget
。__set_name__
會將property
instance
於class
中的名字傳進來。class Property:
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
self._name = ''
def __set_name__(self, owner, name):
self._name = name
...
property
實作有__get__
、__set__
及__delete__
,所以就算沒有給定fset
或fdel
,仍然是一個data descriptor
。__get__
中,先檢查obj
是否為None
。如果是None
的話,則表示是由class
所呼叫,會返回property
instance
本身。接著檢查是否已有指定self.fget
,如果沒有指定的話,則raise AttributeError
。最後呼叫self.fget
執行其getter
的工作。__set__
中,檢查是否已有指定self.fset
,如果沒有指定的話,則raise AttributeError
。最後呼叫self.fset
執行其setter
的工作。__delete__
中,檢查是否已有指定self.fdel
,如果沒有指定的話,則raise AttributeError
。最後呼叫self.fdel
執行其deleter
的工作。class Property:
...
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError(f"property '{self._name}' has no getter")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(f"property '{self._name}' has no setter")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(f"property '{self._name}' has no deleter")
self.fdel(obj)
getter
、setter
與deleter
三種function
的內容非常像。原則就是每次都建立一個新的property
instance
。舉getter
為例, type(self)
其實就是property
這個class
,我們將傳入的fget
指定為Property
的第一個參數fget
,剩餘的self.fset
、self.fdel
及 self.__doc__
就從self
內來取。接著需要手動更新property
instance
的_name
,因為class
內有__set_name__
的attribute
只會在class
被定義時呼叫一次(註1
),所以當我們後續利用getter
、setter
或deleter
介面加入新function
到property
instance
時,需自己更新。這麼一來就可以像是疊加一樣,彈性地加入需要的function
。class Property:
...
def getter(self, fget):
prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def setter(self, fset):
prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def deleter(self, fdel):
prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
prop._name = self._name
return prop
method
與function
不同的點是,method
會自動傳入呼叫它的instance
作為第一個參數,就是我們習慣的self
。當由instance
呼叫在class
中的function
時,其會變成一個bound method
(bound
在self
上)。
types.MethodType
可以幫助我們生成bound method
:
MethodType
的__init__
接受兩個參數,分別為function
與要bound
的對象。MethodType
的__call__
呼叫self.__func__
,並以self.__self__
作為第一個參數,__call__
中所接受*args
及**kwargs
為剩餘參數,會並傳計算結果。class MethodType:
"Emulate PyMethod_Type in Objects/classobject.c"
def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj
def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
return func(obj, *args, **kwargs)
至於function
,因為實作有__get__
,是non-data descriptor
。
function
的__get__
中,先檢查obj
是否為None
。如果是None
的話,則表示是由class
來取,會返回function
instance
本身。如果不是None
的話,則回傳一個MethodType
生成的method
。這個method
是一個bound method
,幫助我們將function
instance
與呼叫其的instance
bound
在一起。class Function:
...
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
if obj is None:
return self
return MethodType(self, obj)
假設我們現在有以下程式碼,我們來拆解看看,呼叫my_instance.my_func(1, 2)
的整個流程。
class MyClass:
def my_func(self, a, b):
...
my_inst = MyClass()
my_inst.my_func(1, 2)
my_inst.my_func
是個non_data_desc
,所以my_inst
會先尋找my_inst.__dict__
中有沒有my_func
。 因為沒有找到,所以會使用my_func
的__get__
。my_inst
來取my_func
,所以會回傳一個MethodType
生的bound method
,這個method
將my_func
與my_inst
bound
在一起。my_inst.my_func(1, 2)
相當於使用bound method
中的__call__
,它會將my_inst
作為my_func
的第一個參數,1
與2
作為my_func
的剩餘參數,然後回傳計算結果。這就是為什麼我們可以使用my_inst.my_func(1, 2)
,而不需使用my_inst.my_func(my_inst, 1, 2)
的由來。一個有趣的事實是,function
的__get__
每次由instance
呼叫時,都會回傳一個新的MethodType
instance
,這代表:
>>> my_inst.my_func is my_inst.my_func # False
>>> my_inst.my_func.__func__ is my_inst.my_func.__func__ # True
或許這會讓您意外,但這正是Python巧妙的設計,底層是同一個function
,但是每次由my_inst.my_func
來取時,都新生成一個bound method
。Welcome to Python!
適合使用staticmethod
的function
,代表其功能與instance
或是class
沒有關係。staticmethod
可以幫忙裝飾底層function
,使得我們無論是由instance
或是class
呼叫,都能使用相同的signature
。
__init__
中,staticmethod
接收一個function
,並利用functools.update_wrapper
來將function
的metadata
更新給staticmethod
的instance
。staticmethod
有實作__get__
,所以是一個non-data descriptor
。無論是由instance
或是由class
來取,都返回self.f
。__call__
中,self.f
不用bound
到任何obj
,直接搭配___call__
接收的*args
及**kwds
,回傳結果即可。import functools
class StaticMethod:
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, objtype=None):
return self.f
def __call__(self, *args, **kwds):
return self.f(*args, **kwds)
classmethod
可以將class
中的function
所bound
的對象,由預設的instance
改為class
。
classmethod
的__init__
與staticmethod
__init__
是一樣的。其接收一個function
,並利用functools.update_wrapper
來將function
的metadata
更新給classmethod
的instance
。classmethod
有實作__get__
,所以是一個non-data descriptor
。當cls
是None
時,代表是由obj
來取,所以利用type(obj)
來取得其cls
。接下來一樣使用MethodType
回傳一個bound method
,只是這次是將self.f
bound
給cls
。__get__
有一段被宣告將廢棄的程式碼,其原意是希望能串聯多個decorator
。但是Python社群實際使用後發現,這樣的用法會產生許多潛在問題,Raymond也指出允許這樣的行為可能是一種錯誤。import functools
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if hasattr(type(self.f), '__get__'):
# This code path was added in Python 3.9
# and was deprecated in Python 3.11.
return self.f.__get__(cls, cls)
return MethodType(self.f, cls)
__slots__
由於__slots__
的實作需要用到C
的structure
及處理記憶體配置,所以Raymond說我們只能盡量仿效,以一個_slotvalues
的list
來替代真正的slot
structure
。
__slots__
的實作比較複雜,共分為五個部份:
Member
class
,此為一個data descriptor
,用來控制已寫在slot_names
內attribute
的存取。Type
metaclass
,其功用為針對slot_names
中所列名字,建立class variable
,並將其值指為相對應的Member
instance
。Object
class
,其功用為設定_slotvalues
(相當於模擬配置__slots__
的記憶體)及當設定或刪除不在slot_names
內的attribute
時,需raise AttributeError
。H
class
,將使用Type
為其metaclass
,並繼承Object
。H
生成h
instance
,實際測試使用。Member
是一個具有__get__
、__set__
及__delete__
的data descriptor
。
__init__
中接收三個變數,分別為其在class
中的名字,class
name
及其位於_slotvalues
中的index
。__get__
中一樣先檢查obj
是否為None
。如果是None
的話,則表示是由class
來取,會返回Member
instance
本身。接著透過self.offset
為index
向obj._slotvalues
取值。如果取回來的是預設的sentinel
值null
的話,表示該index
值沒被正確指定又或者已經被刪除,raise AttributeError
。最後,如果通過上述檢查的話,則返回所取之值。__set__
直接指定value
到obj._slotvalues
的self.offset
位置。__delete__
與__get__
類似。透過self.offset
為index
向obj._slotvalues
取值。如果取回來的是預設的sentinel
值null
的話,表示該index
值沒被正確指定又或者已經被刪除,raise AttributeError
。最後,如果通過上述檢查的話,則將obj._slotvalues[self.offset]
重設為null
。__repr__
中,指定Member
instance
的顯示格式。null = object()
class Member:
def __init__(self, name, clsname, offset):
'Emulate PyMemberDef in Include/structmember.h'
# Also see descr_new() in Objects/descrobject.c
self.name = name
self.clsname = clsname
self.offset = offset
def __get__(self, obj, objtype=None):
'Emulate member_get() in Objects/descrobject.c'
# Also see PyMember_GetOne() in Python/structmember.c
if obj is None:
return self
value = obj._slotvalues[self.offset]
if value is null:
raise AttributeError(self.name)
return value
def __set__(self, obj, value):
'Emulate member_set() in Objects/descrobject.c'
obj._slotvalues[self.offset] = value
def __delete__(self, obj):
'Emulate member_delete() in Objects/descrobject.c'
value = obj._slotvalues[self.offset]
if value is null:
raise AttributeError(self.name)
obj._slotvalues[self.offset] = null
def __repr__(self):
'Emulate member_repr() in Objects/descrobject.c'
return f'<Member {self.name!r} of {self.clsname!r}>'
Type
是一個繼承type
的metaclass
,目的是針對slot_names
中所列出的名字,逐一建立相對的Member
instance
,並加入mapping
中,最後呼叫type.__new__
生成cls
。此舉相當於以slot_names
中的名字,建立class variable
,並將其值指為相對應的Member
instance
。
class Type(type):
'Simulate how the type metaclass adds member objects for slots'
def __new__(mcls, clsname, bases, mapping, **kwargs):
'Emulate type_new() in Objects/typeobject.c'
# type_new() calls PyTypeReady() which calls add_methods()
slot_names = mapping.get('slot_names', [])
for offset, name in enumerate(slot_names):
mapping[name] = Member(name, clsname, offset)
return type.__new__(mcls, clsname, bases, mapping, **kwargs)
Object
class
的目的為被後續class
繼承。
__new__
先利用super().__new__(cls)
生成instance
。接著看看cls
是不是有slot_names
,如果有的話就建立一個長度為len(slot_names)
的list
,並將list
中每個值都預設為null
。接著透過object.__setattr__
將list
設為名為_slotvalues
的instance variable
,並回傳instance
。請注意此處object.__setattr__
的使用實有其必要(註2
)。__setattr__
中會檢查cls
中是否有slot_names
。如果有的話,檢查其名字是否有在cls.slot_names
中,如果不在的話raise AttributeError
。如果通過檢查的話,則delegate
給super().__setattr__
。__delattr__
的邏輯類似__setattr__
。如果沒通過檢查的話raise AttributeError
,有通過的話,則delegate
給super().__delattr__
。class Object:
'Simulate how object.__new__() allocates memory for __slots__'
def __new__(cls, *args, **kwargs):
'Emulate object_new() in Objects/typeobject.c'
inst = super().__new__(cls)
if hasattr(cls, 'slot_names'):
empty_slots = [null] * len(cls.slot_names)
object.__setattr__(inst, '_slotvalues', empty_slots)
return inst
def __setattr__(self, name, value):
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
cls = type(self)
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
raise AttributeError(
f'{cls.__name__!r} object has no attribute {name!r}'
)
super().__setattr__(name, value)
def __delattr__(self, name):
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
cls = type(self)
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
raise AttributeError(
f'{cls.__name__!r} object has no attribute {name!r}'
)
super().__delattr__(name)
H
class
,使用Type
為其metaclass
,並繼承Object
。slot_names
就相當於__slots__
,我們可以將允許的instance variable
名字放進slot_names
這個list
中。
class H(Object, metaclass=Type):
'Instance variables stored in slots'
slot_names = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
可以觀察H.__dict__
,slot_names
及x
與y
都設定好了。
>>> from pprint import pp
>>> pp(dict(vars(H)))
{'__module__': '__main__',
'__doc__': 'Instance variables stored in slots',
'slot_names': ['x', 'y'],
'__init__': <function H.__init__ at 0x7fb5d302f9d0>,
'x': <Member 'x' of 'H'>,
'y': <Member 'y' of 'H'>}
instance
h
可以正常使用,slots
之值存於instance.__dict__
中的_slotvalues
。
>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}
當使用不在slot_names
的名字時,會raise AttributeError
,類似於使用__slots__
的效果。
>>> h.xz
Traceback (most recent call last):
...
AttributeError: 'H' object has no attribute 'xz'
__init_subclass__
改寫__slots__
metaclass
的功能非常強大,對於是否一定要使用其來解決問題,我們會慎之又慎。使用decorator
來裝飾cls
常是避免使用metaclass
的一個方法。自從Python3.6加入__init_subclass__
後,更是大幅度降低需要實作metaclass
的機會。
以下我們嘗試使用__init_subclass__
的方法,來修改上述__slots__
的實作。
MyObject
繼承Object
,並實作有__init_subclass__
。
於__init_subclass__
中:
super().__init_subclass__()
,確保MRO
上的class
如果有實作__init_subclass__
的話,能確實被呼叫。Type.__new__
類似,只是我們這裡是在class
生成後,才 mutate
class
。而Type.__new__
是於生成class
前,就將這些操作放在mapping
。# 01
...
class MyObject(Object):
def __init_subclass__(cls):
'Add member objects for slots'
super().__init_subclass__()
slot_names = cls.__dict__.get('slot_names', [])
clsname = cls.__name__
for offset, name in enumerate(slot_names):
setattr(cls, name, Member(name, clsname, offset))
return cls
此時H
class
只需要繼承MyObject
,而不需要客製的metaclass
。
# 01
...
class H(MyObject):
'Instance variables stored in slots'
slot_names = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
可以觀察H.__dict__
,slot_names
及x
與y
也一樣可以正常設定。
>>> from pprint import pp
>>> pp(dict(vars(H)))
{'__module__': '__main__',
'__doc__': 'Instance variables stored in slots',
'slot_names': ['x', 'y'],
'__init__': <function H.__init__ at 0x00000132D34D9300>,
'x': <Member 'x' of 'H'>,
'y': <Member 'y' of 'H'>}
instance
h
一樣可以正常使用,_slotvalues
也設定無誤。
>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}
當使用不在slot_names
的名字時,一樣會raise AttributeError
。
>>> h.xz
Traceback (most recent call last):
...
AttributeError: 'H' object has no attribute 'xz'. Did you mean: 'x'?
註1:可參考Python docs於此處的敘述。
註2:這邊不能使用inst._slotvalues = empty_slots
或setattr(inst, '_slotvalues', empty_slots)
,因為這兩種語法都相當於使用instance
的__setattr__
。而我們恰恰於Object
實作有__setattr__
,其會在檢查中raise AttributeError
,因為_slotvalues
的確不在cls.slot_names
中。此外,也不能使用super().__setattr__('_slotvalues', empty_slots)
,因為我們是在__new__
中,這相當於super(Object, cls).__setattr__('_slotvalues', empty_slots)
,並不是我們想要的行為。如果一定要使用super()
的話,可以考慮super(Object, inst).__setattr__('_slotvalues', empty_slots)
。但這麼一來有點繞來繞去的,直接使用object.__setattr__
可能更簡單一點。