__slots__、__abstractmethods__、__init_subclass__
May 11, 2022若想控制能指定給物件的屬性名稱,可以在定義類別時指定 __slots__
,若想指明某些特性是抽象方法,可以指定 __abstractmethods__
,如果子類別定義時希望獲得通知,父類別可以實作 __init_subclass__
方法。
__slots__ 屬性清單
__slots__
屬性必須是個字串清單,列出可指定給物件的屬性名稱。例如,若想限制 Some
的實例只能有 a
、b
屬性,可以如下:
>>> class Some:
... __slots__ = ['a', 'b']
...
>>> Some.__dict__.keys()
['a', '__module__', 'b', '__slots__', '__doc__']
>>> s = Some()
>>> s.a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: a
>>> s.a = 10
>>> s.a
10
>>> s.b = 20
>>> s.b
20
>>> s.c = 30
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Some' object has no attribute 'c'
>>>
雖然 __slots__
列出的屬性,就存在於類別的 __dict__
,但在指定屬性給實例前,不可以直接存取該屬性,而且只有 __slots__
列出的屬性,才能被指定給實例。
若類別定義時指定了 __slots__
,從類別建構出來的實例就不會有 __dict__
屬性。例如:
>>> s = Some()
>>> s.__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Some' object has no attribute '__dict__'
>>>
可以在 __slots__
包括 '__dict__'
名稱,讓實例擁有 __dict__
屬性,這麼一來,若指定的屬性名稱不在 __slots__
的清單中,就會被放到自行指定的 __dict__
清單,此時若要列出實例的全部屬性,就要同時包括 __dict__
與 __slots__
中列出的屬性。例如:
>>> class Some:
... __slots__ = ['a', 'b', '__dict__']
...
>>> s = Some()
>>> s.__dict__
{}
>>> s.a = 10
>>> s.b = 20
>>> s.c = 30
>>> s.__dict__
{'c': 30}
>>> for attr in list(s.__dict__) + s.__slots__:
... print(attr, getattr(s, attr))
...
c 30
a 10
b 20
__dict__ {'c': 30}
>>>
__slots__
中的屬性,Python 會實作為描述器。
__slots__
屬性最好被作為類別屬性來使用,尤其是在有繼承關係的場合中,父類別定義的 __slots__
,僅能透過父類別來取得,而子類別的 __slots__
只能透過子類別來取得。
在尋找實例上可設定的屬性時,基本上會對照父類別與子類別中的 __slots__
清單;然而,由於定義了 __slots__
的類別,實例才不會有 __dict__
屬性,因此若父類別沒有定義 __slots__
,子類別即使定義了 __slots__
,以子類別建構出來的實例,仍會具有 __dict__
屬性:
>>> class P:
... pass
...
>>> class C(P):
... __slots__ = ['c']
...
>>> o = C()
>>> o.a = 10
>>> o.b = 10
>>> o.c = 10
>>> o.__dict__
{'a': 10, 'b': 10}
>>>
反之亦然,如果父類別定義了 __slots__
,子類別沒有定義自己的 __slots__
,子類別建構出來的實例也會有 __dict__
。例如:
>>> class P:
... __slots__ = ['c']
...
>>> class C(P):
... pass
...
>>> o = C()
>>> o.a = 10
>>> o.b = 10
>>> o.c = 10
>>> o.__dict__
{'a': 10, 'b': 10}
>>>
__abstractmethods__ 抽象方法
在〈共同行為與 is a〉談過抽象方法,可以透過 abc
模組的 ABC
以及 @abstractmethod
來定義,想知道實現方式的話,必須瞭解 metaclass
的作用及裝飾器的實作,然而就目前可以知道的是,可以定義類別的 __abstractmethods__
,指明某些特性是抽象方法。例如:
>>> class AbstractX:
... def doSome(self):
... pass
... def doOther(self):
... pass
...
>>> AbstractX.__abstractmethods__ = frozenset({'doSome', 'doOther'})
>>> x = AbstractX()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class AbstractX with abstract methods doOther, doSome
>>>
在類別建立之後可指定 __abstractmethods__
屬性,__abstractmethods__
接受集合物件,集合物件中的字串表明哪些方法是抽象方法,如果一個類別的 __abstractmethods__
集合物件不為空,那它是個抽象類別,不可以直接實例化。
然而,子類別看不到父類別的 __abstractmethods__
,因此沒有檢查是否實作了抽象方法的作用,例如:
>>> class ConcreteX(AbstractX):
... pass
...
>>> x = ConcreteX()
>>> ConcreteX.__abstractmethods__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __abstractmethods__
>>>
這是因為 __abstractmethods__
只是個標示,表明目前這個類別會有抽象方法,不可以實例化罷了!
__init_subclass__ 初始子類別
Python 3.6 以後,類別定義時可以建立 __init_subclass__
方法,若有子類別定義,就會呼叫該方法,例如:
>>> class P:
... def __init_subclass__(clz):
... print("子類別" + str(clz))
...
>>> class C(P):
... pass
...
子類別<class '__main__.C'>
>>>
因此這就有了機會,對子類別進行驗證或其他處理,例如,來簡單地檢驗子類別是否實現了抽象方法:
class AbstractX:
def doSome(self):
...
def __init_subclass__(clz):
attrs = vars(clz)
if any(not method in attrs for method in AbstractX.__abstractmethods__):
raise AttributeError(
'必須實作抽象方法' + str(list(AbstractX.__abstractmethods__))
)
AbstractX.__abstractmethods__ = frozenset({'doSome'})
class ConcreteX(AbstractX):
...
若執行這段程式,因為 ConcreteX
沒有實作 doSome
,就會引發以下的錯誤:
Traceback (most recent call last):
File "C:\workspace\test.py", line 13, in <module>
class ConcreteX(AbstractX):
File "C:\workspace\test.py", line 8, in __init_subclass__
raise AttributeError('必須實作抽象方法' + str(list(AbstractX.__abstractmethods__)))
AttributeError: 必須實作抽象方法['doSome']