Python介面與抽象類別之設計:abc模組(Abstract Base Classes)

Python中介面抽象類別之設計:abc模組(Abstract Base Classes)

介面作法如下:

Python介面與抽象類別之設計:abc模組(Abstract Base Classes)

抽象類別作法如下:

Python介面與抽象類別之設計:abc模組(Abstract Base Classes)

01. Quickstart Tutorial

使用 Python abc 模組的原因是為了要解決 Python 沒有「抽象類別 (abstract class)」的問題。透過抽象類別,我們可以建立一個比使用 hasattr() 還要更嚴格的類別介面 (class interface) 檢查。舉例而言,當我們建立一個動物的類別,我們希望之後繼承實作的類別都一定要有screaming」以及「walk」的方法,我們可以透過 abc.ABCMeta 這個 metaclass 來定義 Animal 抽象類別:

import abc

 

class Animal(metaclass=abc.ABCMeta):

    @abc.abstractmethod

    def screaming(self):

        'Return when animal screaming the sound hear likes'

        raise NotImplemented

 

    @abc.abstractmethod

    def walk(self, x, y):

        'Make animal walk to position (x, y).'

        raise NotImplemented

如果不透過 metaclass指定方式,我們也可以透過繼承 abc.ABC 這個 helper class 達到相同的效果:

import abc

 

class Animal(abc.ABC):

    @abc.abstractmethod

    def screaming(self):

        'Return when animal screaming the sound hear likes'

        raise NotImplemented

 

    @abc.abstractmethod

    def walk(self, x, y):

        'Make animal walk to position (x, y).'

        raise NotImplemented

當我們繼承使用 Animal 這個抽象類別來建立類別時,就必須要實作 screaming 以及 walk 如果沒有實作的話,Python 就會產生 TypeError

>>> class Dog(Animal):

...    pass

...

>>> Dog()         # Create a instance*Python實例化物件不使用new關鍵字)

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

TypeError: Can't instantiate abstract class Dog with abstract methods screaming, walk

我們現在來實作 Dog 這個類別:

>>> class Dog(Animal):

...     x = 0

...     y = 0

...     def screaming(self):

...         return 'Wof, Wof'

...     def walk(self, x, y):

...         self.x = x

...         self.y = y

...         return (self.x, self.y)

...

>>> Dog()           # Create a instance

<__main__.Dog object at 0x7fc207b10208>

>>> dog = Dog()

>>> dog.screaming()

'Wof, wof!'

>>> dog.walk(10, 20)

(10, 20)

我們可透過 isinstance 以及 issubclass 方法來檢查 Dog 的類別:

>>> isinstnace(Dog(), Animal)

True

>>> issubclass(Dog, Animal)

True

如果有一天,我們想要把一個Animal視為Python built-in 的 list 类型來使用,我們可以透過 register 來改變這個事實:

>>> isinstance([], Animal)

False

>>> issubclass(list, Animal)

False

>>> Animal.register(list)      # Recognize list as Animal*Animal具有list功能

>>> isinstance([], Animal)

True

>>> issubclass(list, Animal)

True

這個用法在 collections.abc 被大量的使用,CPython collections.abc 中定義了許多的基礎抽象類別,例如說 SequenceIterable 等。現實中符合 Sequence 定義的類別有這些:tuple、str、range、memoryview。我們可以在原始碼看到他們被 Sequence register

Sequence.register(tuple)

Sequence.register(str)

Sequence.register(range)

Sequence.register(memoryview)

接著,我們就可以使用 Sequence 這個抽象類別來比較 list 以及 tuple (*即使 list 以及 tuple 沒有繼承自 Sequence)在這邊我們稱作為 virtual subclasses

>>> from collections import abc

>>> isinstance([], abc.Sequence)

True

>>> isinstnace((), abc.Sequence)

True

>>> issubclass(list, abc.Sequence)

True

>>> issubclass(tuple, abc.Sequence)

True

02. HOW-TO GUIDE

前面提到了抽象方法,那其它的類別屬性也能夠抽象化嗎?答案是可以,自 Python 3.3 版後,只需要在 method decorator後再加上 @abstractmethod 即可

  • 抽象化 classmethod 以及 staticmethod

import abc

 

class Base(abc.ABC): 

    @classmethod

    @abc.abstractmethod

    def setUpClass(cls):

        raise NotImplemented

 

    @staticmethod

    @abc.abstractmethod

    def count(self, data):

        raise NotImplemented

  

class Implementation(Base): 

    @classmethod

    def setUpClass(cls):

        cls.count = 0

 

    @staticmethod

    def count(self, data):

        self.count = len(data)

        return self.count

  • 抽象化 property

class 裡面的 property 也可以要求抽象化,同樣在 decorator後再加上 @abstractmethod 即可:

import abc

  

class Base(abc.ABC):

    _index = 0

 

    @property     

    @abc.abstractmethod

    def index(self):

        raise NotImplemented

 

    @index.setter

    @abc.abstractmethod

    def index(self, new_index):

        raise NotImplemented

  

class Implementation(Base):

    MAX_LEN = 100

 

    @property

    def index(self):

        return self._index

 

    @index.setter

    def index(self, new_index):

        new_index = min(new_index, self.MAX_LEN)

        self._index = new_index

 

imp = Implementation()

print(imp.index)

imp.index = 50

print(imp.index)

imp.index = 500

print(imp.index)

03. DISCUSSIONS

> abc 的運作原理?

abc.ABCMeta 主要可做到三件事情:

  1.     ABCMeta metaclass,因此 override __new__ 方法內會初始化 abstract 檢查的部分
  2.     override __isinstancecheck__
  3.     override __issubclasscheck__

> 如何做到 decorator + abstractmethod 而不需要相對應的 abstract decorator (即@abstractclassmethod…etc)?

請參考 commit: bfebb7b54a50f011 以及 bpo-11610

簡而言之,透過新增加 C-API _PyObject_IsAbstract(),在 descrobject 以及 funcobject tp_getset slot 裡面檢查是否有 __isabstractmethod__ 的參數即可。分別在 object.c 加入_PyObject_IsAbstract()descrobject.c 加入 property_getsetlistfuncobject.c加入 cm_getsetlist 以及 sm_getsetlist 到 tp_getset slot。

> abc.ABC helper class 的運作原理?

abc.ABC 程式碼如下:

class ABC(metaclass=ABCMeta):

    """Helper class that provides a standard way to create an ABC using

    inheritance.

    """

    __slots__ = ()

透過 metaclass=ABCMeta 來指定metaclass 的 method,或讓一般 class 繼承 ABC時也具有 ABCMeta 的功能。接者把 __slots__ 設為 (),這樣的設定讓其他人無法修改 ABC instance 的 attributes

>>> import abc

>>> class Foo(object):

...     pass

...

>>> foo = Foo()

>>> foo.bar = 10     #

>>> print(foo.bar)

10

>>> a = abc.ABC()

>>> a.bar = 10    #不可

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: 'ABC' object has no attribute 'bar'

有關 __slots__ 的介紹,請參考 __slots__ 以及 使用 __slots__

 

Python中介面抽象類別之設計:abc模組(Abstract Base Classes)

介面作法如下:

Python介面與抽象類別之設計:abc模組(Abstract Base Classes)

抽象類別作法如下:

Python介面與抽象類別之設計:abc模組(Abstract Base Classes)

01. Quickstart Tutorial

使用 Python abc 模組的原因是為了要解決 Python 沒有「抽象類別 (abstract class)」的問題。透過抽象類別,我們可以建立一個比使用 hasattr() 還要更嚴格的類別介面 (class interface) 檢查。舉例而言,當我們建立一個動物的類別,我們希望之後繼承實作的類別都一定要有screaming」以及「walk」的方法,我們可以透過 abc.ABCMeta 這個 metaclass 來定義 Animal 抽象類別:

import abc

 

class Animal(metaclass=abc.ABCMeta):

    @abc.abstractmethod

    def screaming(self):

        'Return when animal screaming the sound hear likes'

        raise NotImplemented

 

    @abc.abstractmethod

    def walk(self, x, y):

        'Make animal walk to position (x, y).'

        raise NotImplemented

如果不透過 metaclass指定方式,我們也可以透過繼承 abc.ABC 這個 helper class 達到相同的效果:

import abc

 

class Animal(abc.ABC):

    @abc.abstractmethod

    def screaming(self):

        'Return when animal screaming the sound hear likes'

        raise NotImplemented

 

    @abc.abstractmethod

    def walk(self, x, y):

        'Make animal walk to position (x, y).'

        raise NotImplemented

當我們繼承使用 Animal 這個抽象類別來建立類別時,就必須要實作 screaming 以及 walk 如果沒有實作的話,Python 就會產生 TypeError

>>> class Dog(Animal):

...    pass

...

>>> Dog()         # Create a instance*Python實例化物件不使用new關鍵字)

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

TypeError: Can't instantiate abstract class Dog with abstract methods screaming, walk

我們現在來實作 Dog 這個類別:

>>> class Dog(Animal):

...     x = 0

...     y = 0

...     def screaming(self):

...         return 'Wof, Wof'

...     def walk(self, x, y):

...         self.x = x

...         self.y = y

...         return (self.x, self.y)

...

>>> Dog()           # Create a instance

<__main__.Dog object at 0x7fc207b10208>

>>> dog = Dog()

>>> dog.screaming()

'Wof, wof!'

>>> dog.walk(10, 20)

(10, 20)

我們可透過 isinstance 以及 issubclass 方法來檢查 Dog 的類別:

>>> isinstnace(Dog(), Animal)

True

>>> issubclass(Dog, Animal)

True

如果有一天,我們想要把一個Animal視為Python built-in 的 list 类型來使用,我們可以透過 register 來改變這個事實:

>>> isinstance([], Animal)

False

>>> issubclass(list, Animal)

False

>>> Animal.register(list)      # Recognize list as Animal*Animal具有list功能

>>> isinstance([], Animal)

True

>>> issubclass(list, Animal)

True

這個用法在 collections.abc 被大量的使用,CPython collections.abc 中定義了許多的基礎抽象類別,例如說 SequenceIterable 等。現實中符合 Sequence 定義的類別有這些:tuple、str、range、memoryview。我們可以在原始碼看到他們被 Sequence register

Sequence.register(tuple)

Sequence.register(str)

Sequence.register(range)

Sequence.register(memoryview)

接著,我們就可以使用 Sequence 這個抽象類別來比較 list 以及 tuple (*即使 list 以及 tuple 沒有繼承自 Sequence)在這邊我們稱作為 virtual subclasses

>>> from collections import abc

>>> isinstance([], abc.Sequence)

True

>>> isinstnace((), abc.Sequence)

True

>>> issubclass(list, abc.Sequence)

True

>>> issubclass(tuple, abc.Sequence)

True

02. HOW-TO GUIDE

前面提到了抽象方法,那其它的類別屬性也能夠抽象化嗎?答案是可以,自 Python 3.3 版後,只需要在 method decorator後再加上 @abstractmethod 即可

  • 抽象化 classmethod 以及 staticmethod

import abc

 

class Base(abc.ABC): 

    @classmethod

    @abc.abstractmethod

    def setUpClass(cls):

        raise NotImplemented

 

    @staticmethod

    @abc.abstractmethod

    def count(self, data):

        raise NotImplemented

  

class Implementation(Base): 

    @classmethod

    def setUpClass(cls):

        cls.count = 0

 

    @staticmethod

    def count(self, data):

        self.count = len(data)

        return self.count

  • 抽象化 property

class 裡面的 property 也可以要求抽象化,同樣在 decorator後再加上 @abstractmethod 即可:

import abc

  

class Base(abc.ABC):

    _index = 0

 

    @property     

    @abc.abstractmethod

    def index(self):

        raise NotImplemented

 

    @index.setter

    @abc.abstractmethod

    def index(self, new_index):

        raise NotImplemented

  

class Implementation(Base):

    MAX_LEN = 100

 

    @property

    def index(self):

        return self._index

 

    @index.setter

    def index(self, new_index):

        new_index = min(new_index, self.MAX_LEN)

        self._index = new_index

 

imp = Implementation()

print(imp.index)

imp.index = 50

print(imp.index)

imp.index = 500

print(imp.index)

03. DISCUSSIONS

> abc 的運作原理?

abc.ABCMeta 主要可做到三件事情:

  1.     ABCMeta metaclass,因此 override __new__ 方法內會初始化 abstract 檢查的部分
  2.     override __isinstancecheck__
  3.     override __issubclasscheck__

> 如何做到 decorator + abstractmethod 而不需要相對應的 abstract decorator (即@abstractclassmethod…etc)?

請參考 commit: bfebb7b54a50f011 以及 bpo-11610

簡而言之,透過新增加 C-API _PyObject_IsAbstract(),在 descrobject 以及 funcobject tp_getset slot 裡面檢查是否有 __isabstractmethod__ 的參數即可。分別在 object.c 加入_PyObject_IsAbstract()descrobject.c 加入 property_getsetlistfuncobject.c加入 cm_getsetlist 以及 sm_getsetlist 到 tp_getset slot。

> abc.ABC helper class 的運作原理?

abc.ABC 程式碼如下:

class ABC(metaclass=ABCMeta):

    """Helper class that provides a standard way to create an ABC using

    inheritance.

    """

    __slots__ = ()

透過 metaclass=ABCMeta 來指定metaclass 的 method,或讓一般 class 繼承 ABC時也具有 ABCMeta 的功能。接者把 __slots__ 設為 (),這樣的設定讓其他人無法修改 ABC instance 的 attributes

>>> import abc

>>> class Foo(object):

...     pass

...

>>> foo = Foo()

>>> foo.bar = 10     #

>>> print(foo.bar)

10

>>> a = abc.ABC()

>>> a.bar = 10    #不可

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: 'ABC' object has no attribute 'bar'

有關 __slots__ 的介紹,請參考 __slots__ 以及 使用 __slots__