Python抽象基類:ABC謝謝你,因為有你,溫暖了四季!
最近閱讀了《Python Tricks: The Book》的第四章“Classes & OOP”,這一章節介紹了Python對物件導向程式設計的支援,內容包括“is”和“==”的區別、特殊方法“__str__”和“__repr__”的作用、自定義異常類、淺拷貝深拷貝、抽象基類以及例項方法和類方法的區別等。本文記錄自己學習這些知識點的心得體會,重點討論其中的例項方法、類方法和靜態方法的應用場景、抽象類以及具名元組等內容。
例項方法、類方法和靜態方法
例項方法、類方法和靜態方法是三個不同的概念:
-
例項方法用於訪問和修改類例項的屬性,它的第一個引數是“self”。當通過類名呼叫例項方法時,需要提供類的例項作為引數(傳一個物件作為引數);當通過類的例項呼叫例項方法時,Python會自動把例項方法繫結到呼叫方,例項方法的“self”引數就是呼叫它的類例項(可以類比functools.partial?):
class Pizza: def __init__(self, size: int): self.size = size def get_size(self) -> int: return self.size Pizza.get_size() # TypeError, missing argument: 'self' Pizza.get_size(Pizza(25)) # 25 Pizza(21).get_size() # 21,不用顯式指定例項方法的“self”引數啦
-
類方法常被用作工廠方法,用於建立類的例項(可以類比Java中類的構造方法),它的第一個引數是“cls”。類的例項可能有多種構造方案,比如時間類,既可以通過指定年月日時分秒構造例項,也可以通過解析字串“%Y-%m-%d %H:%M:%S”進行構造,而Python規定了一個類只能定義一個初始化方法“__init__”,這限制了根據類建立例項的靈活性。類方法是緩解該矛盾的有效方案:
from typing import List class Pizza: def __init__(self, ingredients: List[str]): self.ingredients = ingredients def __repr__(self): # 定義把物件轉化為字串的方法,!r表示呼叫變數的__repr__方法 return f'Pizza({self.ingredients!r})' @classmethod # 類方法使用@classmethod裝飾器修飾 def margherita(cls): """"瑪格麗塔,一種披薩的名稱""" return cls(['mozzarella', 'tomatoes']) @classmethod def prosciutto(cls): """火腿披薩""" return cls(['mozzarella', 'tomatoes', 'ham']) Pizza(['mozzarella', 'tomatoes']) Pizza.margherita() # Pizza(['mozzarella', 'tomatoes']) Pizza.prosciutto() # Pizza(['mozzarella', 'tomatoes', 'ham'])
-
靜態方法跟定義在相同模組中的函式沒有明顯的區別,它不依賴於類變數或例項物件的狀態,需要使用“@staticmethod”裝飾器修飾:
from typing import List import math class Pizza: def __init__(self, ingredients: List[str]): self.ingredients = ingredients @staticmethod def circle_area(radius: float) -> float: return radius ** 2 * math.pi pizza = Pizza(['mozzarella', 'tomatoes']) pizza.circle_area(4) # 50.27 Pizza.circle_area(4) # 50.27
抽象類
抽象類是指宣告瞭抽象方法的類,它不能被例項化,但可以被繼承。當抽象類被繼承時,子類往往需要實現父類的所有抽象方法(如果只實現部分抽象方法,那子類也是抽象類)。抽象類常用於型別檢查,即判斷給定的類是否由某個基類派生(issubclass())或給定的物件是否為某個基類的例項(isinstance())。
import collections
issubclass(list, collections.abc.Iterable) # True
issubclass(list, collections.abc.Hashable) # False
a = [1, 0, 2, 4]
isinstance(a, collections.abc.Sequence) # True
isinstance(a, collections.abc.Mapping) # False
此外,抽象類的使用能夠讓類的層次關係變得清晰,方便程式碼的開發和維護;抽象類宣告介面,子類給出具體實現,子類例項的行為變得可以預期。舉例來說,抽象類“collections.abc.Sized”宣告瞭抽象方法“__len__”,Python的內建型別list、tuple、set、dict等都繼承自該抽象類,因此可以通過“len()”函式獲取這些型別的例項的大小。
早期Python通過在方法的定義體中丟擲“NotImplementedError”異常的方式來宣告抽象方法,抽象基類(Abstract Base Classes,ABC)出現以後,有了更好的方案。
-
通過丟擲“NotImplementedError”異常定義抽象類
class Base: """基於NotImplementedError異常的抽象類""" def foo(self): raise NotImplementedError() b = Base() # 雖然名為抽象類,但還是可以被例項化
-
通過繼承“abc.ABC”定義抽象類
import abc # 匯入抽象基類模組 class Base(abc.ABC): """"通過繼承abc.ABC定義抽象類""" @abc.abstractmethod def foo(self): """"抽象方法使用@abc.abstractmethod裝飾器標記""" def bar(self): pass # 抽象類可以包含具體方法 class Concrete(Base): def foo(self): print('聽我說謝謝你,因為有你,溫暖了四季~') b = Base() # TypeError: Can't instantiate abstract class Base with abstract methods foo c = Concrete() c.foo() # 聽我說謝謝你,因為有你,溫暖了四季~
具名元組
元組是不可變的列表,常被用於表示資料的記錄(類比Java的Record型別?關係型資料庫也把表中的一行資料稱為元組)。元組中的元素只能通過索引進行訪問,而整型的索引難以表示元素在資料記錄中的語義,最終導致程式碼的可讀性變差。為了解決該問題,Python提出了具名元組(Named Tuple),允許通過可讀性強的識別符號訪問元組中的元素。有兩種定義具名元組的方式:(1)利用“collections.namedtuple”工廠函式定義具名元組;(2)通過繼承“typing.NamedTuple”定義具名元組。
-
利用“collections.namedtuple”工廠函式定義具名元組
namedtuple工廠函式接收一個識別符號和欄位列表作為引數,返回以該識別符號命名的類(是內建型別tuple的子類)。
from collections import namedtuple Car = namedtuple('Car' , ['color', 'mileage']) tesla = Car('black', mileage=376.5) # 建立例項時,提供位置引數、關鍵字引數均可 tesla.color # 'black',現在可以通過識別符號訪問元組的元素啦 tesla._asdict() # {'color': 'black', 'mileage': 376.5} tesla._replace(color='white') # Car(color='white', mileage=376.5),通過替換元素構造新的元組 Car._make(['white', 376.5]) # Car(color='white', mileage=376.5),通過類方法構造新的元組 Car._fields # ('color', 'mileage')
-
通過繼承“typing.NamedTuple”定義具名元組
import typing class Car(typing.NamedTuple): # 繼承typing.NamedTuple color: str mileage: float tesla = Car('black', mileage=376.5) tesla.color # 'black' tesla._asdict() # {'color': 'black', 'mileage': 376.5} Car._fields # ('color', 'mileage') Car._field_types # {'color': str, 'mileage': float},相比於利用工廠函式定義的方式多了型別資訊
具名元組佔用的空間與內建的元組型別是相同的,這是我不能理解的地方。儘管欄位名列表、型別資訊可以繫結到類上面,但是諸如“_asdict()”、“_replace()”這樣的例項方法需要繫結到具名元組的例項上,這為什麼沒有帶來空間開銷呢?還是說通過“sys.getsizeof()”獲得的物件佔用記憶體空間的大小跟想的是不太一樣?
import sys
a = ('black', 376.5)
sys.getsizeof(a) # 56,普通元組佔用56個位元組的空間
b = Car('black', 376.5)
sys.getsizeof(b) # 56,具名元組同樣佔用56個位元組的空間
除了具名元組,定義資料類的另一種方式是使用“dataclasses”模組,由於篇幅限制,這裡就不再展開介紹了。