介面、實現、多型
轉載須註明出處: ofollow,noindex">https://www.jianshu.com/u/5e6f798c903a
參考:《資料結構(Python 語言描述)》 - 第5章 介面、實現和多型

目錄.jpg
在 C# 等靜態語言中,我們會將"介面型別"簡稱為介面。但本文中提及的"介面"並非特指介面型別,而是以軟體資源使用者的視角,將軟體資源為使用者提供的各種操作視為介面。由於使用者只關心如何使用方法,但並不關心其具體實現(這是類編寫者的工作),所以介面僅含方法名、引數以及文件註釋。就 Python 而言,介面可理解為 "類為使用者提供的一組方法簽名及其文件註釋" 。
使用 help
函式獲取模組、資料型別、類、方法或函式的相關資訊時,便會訪問與該軟體資源的介面相關的文件。比如通過 help()
獲取 data types (或 classes) 的幫助文件時,可以看到一個 "Methods defined" 列表,其中包含了相關的介面。
class Cls: """測試類""" def func_1(self, argm: object) -> object: """方法 1""" return True def func_2(self, argm): """方法 2""" pass help(Cls)
輸出:
class Cls(builtins.object) |測試類 | |Methods defined here: | |func_1(self, argm:object) -> object |方法 1 | |func_2(self, argm) |方法 2 | |---------------------------------------------------------------------- |Data descriptors defined here: | |__dict__ |dictionary for instance variables (if defined) | |__weakref__ |list of weak references to the object (if defined)
一個設計良好的軟體資源,其特徵之一便是可以清晰的區分介面和實現。當用戶使用一個軟體資源時,只需要關心其介面。理想情況下,這些軟體資源如何實現的細節,也就是底層的演算法程式碼和資料結構,都隱藏或封裝在一個抽象的邊界中。這個邊界將介面和實現隔開,從而:
- 允許使用者即插即用的方式,快速地將軟體資源整合起來;
- 讓使用者有機會在相同軟體資源的不同實現中做出選;
- 並允許具體實現者對軟體資源的實現做出修改,而不影響其使用者的程式碼。
多型(polymorphism)是指一個軟體資源的多種實現,但這些實現都遵從相同的介面或方法。比如在兩個不同的實現中都使用了相同的運算子符號或方法名,這便是多型。多型方法的示例是 str
和 len
;多型運算子的示例是 +
和 ==
。
1. 介面型別
一些程式語言會提供介面(interface)型別,例如 C#。在介面型別中,我們會指定一組函式成員,但不會實現它們。因此,介面本身不會執行任何操作,它作用是為實現介面的類提供一套有關函式成員的藍圖。在建立類的過程中,必須依照介面提供的藍圖,逐一實現介面中的函式成員。
Tips:本小結中提及的"介面"表示介面型別,如 IIfc1
介面。
# 本例展示了在C#中宣告和使用介面的過程 interface IIfc1{// 宣告介面 void PrintOut(string s); // 不必實現函式體 } class MyClass : IIfc1{ // 宣告類,該類需要實現IIfc1介面 public void PrintOut(string s){ // 實現介面 Console.WriteLine("Calling through: {0}", s); } } class Program{ static void Main(){ MyClass mc = new MyClass(); // 建立類例項 mc.PrintOut("object"); // 呼叫方法 } }
2. 偽介面類
Python 中並沒有介面型別,如果我們需要事先為類提供一個藍圖,以說明類中所包含的操作,可建立一個偽介面類。偽介面類不會出現在繼承鏈中,作用僅在於為類提供藍圖,用於說明類中所包含的操作,並確保操作和實現之間的一致性。
偽介面類通常以 "Interface" 為字尾,其中包含一組未實現的方法,所有方法都以 pass
或 return
結尾幷包含文件字串。 pass
語句用於沒有返回值的修改器方法; return
語句用於訪問器方法,並返回一個簡單的預設值,如 False
、 0
或 None
。最後,還需要為所有方法新增文件字串,以說明該方法所做的事情。
由於在之後的內容中,我們會為一種為名"包(bag)"的無需集合(collection)型別開發介面。所以,此處以包集合為例,建立一個偽介面類:
""" File: baginterface.py Author: Ken Lambert """ class BagInterface(object): """Interface for all bag types.""" # Constructor 構造器 def __init__(self, sourceCollection = None): """Sets the initial state of self, which includes the contents of sourceCollection, if it's present.""" pass # Accessor methods 訪問器方法 def isEmpty(self): """Returns True if len(self) == 0, or False otherwise.""" return True def __len__(self): """Returns the number of items in self.""" return 0 def __str__(self): """Returns the string representation of self.""" return "" def __iter__(self): """Supports iteration over a view of self.""" return None def __add__(self, other): """Returns a new bag containing the contents of self and other.""" return None def __eq__(self, other): """Returns True if self equals other, or False otherwise.""" return False # Mutator methods def clear(self): """Makes self become empty.""" pass def add(self, item): """Adds item to self.""" pass def remove(self, item): """Precondition: item is in self. Raises: KeyError if item in not in self. Postcondition: item is removed from self.""" pass
在設計一個軟體資源的偽介面類時,首先考慮要完成哪些操作,並依次給出文件字串;然後,依據文件字串的描述來給出恰當的函式簽名。
3. 開發基於 BagInterface 的實現
下面來為包集合開發兩種基於 BagInterface 的實現。
在集合類的設計者獲知了集合的偽介面類之後,類自身的設計和實現包含兩個步驟:
__init__
注意:偽介面類的作用僅是為實現提供藍圖,不必也不需要繼承偽介面類。
3.1 基於陣列的實現
本節將基於 BagInterface 開發一個基於陣列的實現,這裡使用名為 Array 的陣列類,程式碼如下:
""" File: arrays.py An Array is a restricted list whose clients can use only [], len, iter, and str. To instantiate, use <variable> = array(<capacity>, <optional fill value>) The fill value is None by default. """ class Array(object): """Represents an array.""" def __init__(self, capacity, fillValue = None): """Capacity is the static size of the array. fillValue is placed at each position.""" self._items = list() for count in range(capacity): self._items.append(fillValue) def __len__(self): """-> The capacity of the array.""" return len(self._items) def __str__(self): """-> The string representation of the array.""" return str(self._items) def __iter__(self): """Supports iteration over a view of an array.""" return iter(self._items) def __getitem__(self, index): """Subscript operator for access at index.""" return self._items[index] def __setitem__(self, index, newItem): """Subscript operator for replacement at index.""" self._items[index] = newItem
a. 構造器方法
from arrays import Array class ArrayBag(object):# 不必繼承BagInterface """An array-based bag implementation.""" # Class variable DEFAULT_CAPACITY = 10 # Constructor def __init__(self, sourceCollection = None): """Sets the initial state of self, which includes the contents of sourceCollection, if it's present.""" self._items = Array(ArrayBag.DEFAULT_CAPACITY) self._size = 0 # _size表示邏輯尺寸,len(Array)表示物理尺寸 if sourceCollection: for item in sourceCollection: self.add(item) # --snip--
b. 無需使用陣列變數的方法
當需要訪問(或修改)例項變數時,應儘可能通過相應的方法來完成操作,將對變數的引用保持最小化。比如當我們需要檢視包例項的邏輯尺寸時,應該執行 len()
,而不是直接使用例項變數 _size
。
這樣做的優點是,例如某個方法沒有使用陣列變數,那麼即使將陣列變為其它資料結構,也無需對該方法做出修改,可繼續使用。
class ArrayBag(object): # --snip-- # Accessor methods def isEmpty(self): """Returns True if len(self) == 0, or False otherwise.""" return len(self) == 0 def __len__(self): """Returns the number of items in self.""" return self._size def __str__(self): """Returns the string representation of self.""" return "{" + ", ".join(map(str, self)) + "}" def __eq__(self, other): """Returns True if self equals other, or False otherwise.""" if self is other: return True if type(self) != type(other) or \ len(self) != len(other): return False for item in self: if not item in other: return False return True # --snip--
c. 需要使用陣列變數的方法
如果將陣列變為其它資料結構,則會影響到使用陣列變數的方法。因為,資料結構的改變可能會導致操作方式的變化。
class ArrayBag(object): # --snip-- # Accessor methods def __iter__(self): """Supports iteration over a view of self.""" cursor = 0 while cursor < len(self): yield self._items[cursor] # 使用陣列變數 cursor += 1 def __add__(self, other): """Returns a new bag containing the contents of self and other.""" result = ArrayBag(self) # 使用陣列變數,不一定是類變數 for item in other: result.add(item) return result # Mutator methods def clear(self): """Makes self become empty.""" self._items = Array(ArrayBag.DEFAULT_CAPACITY) # 使用陣列變數 self._size = 0 def add(self, item): """Adds item to self.""" # Check array memory here and increase it if necessary # 附加練習:如果陣列已滿,需要編寫額外的程式碼以調整陣列大小 self._items[len(self)] = item # 使用陣列變數 self._size += 1 def remove(self, item): """Precondition: item is in self. Raises: KeyError if item in not in self. Postcondition: item is removed from self.""" # Check precondition and raise if necessary if not item in self: raise KeyError(str(item) + " not in bag") # Search for the index of the target item targetIndex = 0 for targetItem in self: if targetItem == item: break targetIndex += 1 # Shift items to the left of target up by one position for i in range(targetIndex, len(self) - 1): self._items[i] = self._items[i + 1] # 使用陣列變數 # Decrement logical size self._size -= 1 # Check array memory here and decrease it if necessary # 附加練習:如果刪除了多個項,可能會造成記憶體的浪費。 # 編寫程式碼,當陣列的裝載因子達到某個閾值時,自動調整陣列的大小
3.2 基於連結串列的實現
本節將基於 BagInterface 開發一個基於連結串列的實現,這裡使用名為 Node 的連結串列類,程式碼如下:
""" File: node.py Author: Ken Lambert """ class Node(object): """Represents a singly linked node.""" def __init__(self, data, next = None): self.data = data self.next = next
a. 構造器方法
""" File: linkedbag.py Author: Ken Lambert """ from node import Node class LinkedBag(object): """A link-based bag implementation.""" # Constructor def __init__(self, sourceCollection = None): """Sets the initial state of self, which includes the contents of sourceCollection, if it's present.""" self._items = None # 外部頭連結,初始值None表明連結串列為空 self._size = 0 # 邏輯尺寸 if sourceCollection: for item in sourceCollection: self.add(item) # --snip--
b. 可拷貝的方法
如果某個方法沒有訪問陣列變數,它也就不必訪問連結串列變數。所以那些 ArrayBag 類中沒有訪問陣列變數的方法,可直接拷貝到 LinkedBag 中。這是之前的策略——當需要訪問(或修改)例項變數時,應儘可能通過相應的方法來完成操作,將對變數的引用保持最小化——帶來的回報。也是編寫方法的重要一環:儘可能嘗試著將資料結構隱藏到物件的方法呼叫之中,以使得通過呼叫相應的方法便可完成對資料結構的操作(always try to hide the implementing data structures behind a wall of method calls on the object being implemented.)。
以下方法沒有訪問陣列變數,可直接拷貝到 LinkedBag 中:
class LinkedBag(object): # --snip-- # Accessor methods def isEmpty(self): """Returns True if len(self) == 0, or False otherwise.""" return len(self) == 0 def __len__(self): """Returns the number of items in self.""" return self._size def __str__(self): """Returns the string representation of self.""" return "{" + ", ".join(map(str, self)) + "}" def __eq__(self, other): """Returns True if self equals other, or False otherwise.""" if self is other: return True if type(self) != type(other) or \ len(self) != len(other): return False for item in self: if not item in other: return False return True # --snip--
c. 需要使用連結串列變數的方法
class LinkedBag(object): # --snip-- # Accessor methods def __iter__(self): """Supports iteration over a view of self.""" cursor = self._items while not cursor is None: yield cursor.data cursor = cursor.next def __add__(self, other): """Returns a new bag containing the contents of self and other.""" result = LinkedBag(self) # 改動 for item in other: result.add(item) return result # Mutator methods def clear(self): """Makes self become empty.""" # Exercise self._items = None# 外部頭連結,初始值None表明連結串列為空 self._size = 0# 邏輯尺寸 def add(self, item): """Adds item to self's head.""" # 在列表頭部新增新項,從而利用了常數訪問時間的優點 self._items = Node(item, self._items) self._size += 1 def remove(self, item): """Precondition: item is in self. Raises: KeyError if item in not in self. Postcondition: item is removed from self.""" # Check precondition and raise if necessary if not item in self: raise KeyError(str(item) + " not in bag") # Search for the node containing the target item # probe will point to the target node, and trailer # will point to the one before it, if it exists probe = self._items trailer = None for targetItem in self: if targetItem == item: break trailer = probe probe = probe.next # Unhook the node to be deleted, either the first one or one # thereafter if probe == self._items: self._items = self._items.next else: trailer.next = probe.next # Decrement logical size self._size -= 1 # --snip--