__slots__、__abstractmethods__、__init_subclass__

May 11, 2022

若想控制能指定給物件的屬性名稱,可以在定義類別時指定 __slots__,若想指明某些特性是抽象方法,可以指定 __abstractmethods__,如果子類別定義時希望獲得通知,父類別可以實作 __init_subclass__ 方法。

__slots__ 屬性清單

__slots__ 屬性必須是個字串清單,列出可指定給物件的屬性名稱。例如,若想限制 Some 的實例只能有 ab 屬性,可以如下:

>>> 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']

分享到 LinkedIn 分享到 Facebook 分享到 Twitter