Python中的鴨子類型

今天,我們來聊一聊Python中的鴨子類型(duck typing)。

編程語言具有類型概念,例如Python中有數字類型、字符串類型、布爾類型,或者更加複雜的結構,例如元組tuple、列表list、集合set和字典類型dict等等。

根據如何將類型解析並賦值給各種構造(例如變量,表達式,函數,函數參數等),編程語言可以歸類為「鴨子類型」,「結構化類型」或「標稱類型」。

本質上,分類決定了對象如何被解析並推斷為具體的類型。

鴨子測試

鴨子類型(duck typing)語言使用鴨子測試來評估對象是否可以被解析為特定的類型。Python就是其中一種。

這個概念的名字來源於由詹姆斯·惠特科姆·萊利提出的鴨子測試。「鴨子測試」可以這樣表述:

「如果看起來像鴨子,叫起來像鴨子,那麼它一定是鴨子。」

If it looks like a duck and quacks like a duck, it must be a duck.

James Whitcomb Riley

image

鴨子類型

鴨子類型在程序設計中是動態類型的一種風格。在這種風格中,一個對象有效的語義,不是由繼承自特定的類或實現特定的接口,而是由”當前方法和屬性的集合”決定。

在鴨子類型中,關注點在於對象的行為,能作什麼;而不是關注對象所屬的類型。例如,在不使用鴨子類型的語言中,我們可以編寫一個函數,它接受一個類型為”鴨子”的對象,並調用它的”走”和”叫”方法。在使用鴨子類型的語言中,這樣的一個函數可以接受一個任意類型的對象,並調用它的”走”和”叫”方法。如果這些需要被調用的方法不存在,那麼將引發一個運行時錯誤。任何擁有這樣的正確的”走”和”叫”方法的對象都可被函數接受的這種行為引出了以上表述,這種決定類型的方式因此得名。

鴨子類型通常得益於”不”測試方法和函數中參數的類型,而是依賴文檔、清晰的代碼和測試來確保正確使用。

在常規類型中,我們能否在一個特定場景中使用某個對象取決於這個對象的類型,而在鴨子類型中,則取決於這個對象是否具有某種屬性或者方法——即只要具備特定的屬性或方法,能通過鴨子測試,就可以使用。

Python術語

Python術語表中的鴨子類型duck-typing

A programming style which does not look at an object』s type to determine if it has the right interface; instead, the method or attribute is simply called or used (「If it looks like a duck and quacks like a duck, it must be a duck.」) By emphasizing interfaces rather than specific types, well-designed code improves its flexibility by allowing polymorphic substitution. Duck-typing avoids tests using type() or isinstance(). (Note, however, that duck-typing can be complemented with abstract base classes.) Instead, it typically employs hasattr() tests or EAFP programming.

譯文:

鴨子類型是一種編程風格,決定一個對象是否有正確的接口,關注點在於它的方法或屬性,而不是它的類型(「如果它看起來像鴨子,像鴨子一樣嘎嘎叫,那麼它一定是鴨子。」)。通過強調接口而不是特定類型,設計良好的代碼通過多態提高了靈活性。鴨子類型無需使用 type()isinstance() 進行檢查(注意,鴨子類型可以用抽象基類來補充),相反,它通常使用 hasattr()來檢查,或是 EAFP 編程。

代碼示例

舉個例子:

# 定義一個計算函數,接收3個入參
def calculate(a, b, c):
    return (a + b) * c


# 分別計算3種情況的結果
result1 = calculate(1, 2, 3)
result2 = calculate([1, 2, 3], [4, 5, 6], 2)
result3 = calculate("打工人", "打工魂", 3)

# 打印3種結果
print(result1, result2, result3, sep='\n')

代碼解釋:在上面的例子中,我們每次調用calculate()都使用的是不同的對象(數字、列表和字符串),並且它們在繼承關係中沒有聯繫。只要輸入對象支持+*方法,操作就能成功。

執行後輸出:

9
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
打工人打工魂打工人打工魂打工人打工魂

這樣,鴨子類型在不使用繼承的情況下使用了多態。

再舉個栗子:

# 鴨子類
class Duck:

    def quack(self):
        print("這鴨子正在嘎嘎叫")

    def feathers(self):
        print("這鴨子擁有白色和灰色的羽毛")

# 人類
class Person:

    def quack(self):
        print("這人正在模仿鴨子")

    def feathers(self):
        print("這人在地上拿起1根羽毛然後給其他人看")


# 函數/接口
def in_the_forest(duck):
    duck.quack()
    duck.feathers()


if __name__ == '__main__':

    donald = Duck()  # 創建一個Duck類的實例
    john = Person()  # 創建一個Person類的實例

    in_the_forest(donald)  # 調用函數,傳入Duck的實例
    in_the_forest(john)    # 調用函數,傳入Person的實例

代碼運行後輸出:

這鴨子正在嘎嘎叫
這鴨子擁有白色和灰色的羽毛
這人正在模仿鴨子
這人在地上拿起1根羽毛然後給其他人看

總結

鴨子類型是一個與動態類型(dynamic typing)相關的概念,其中關注的是它定義的方法,而不太關注對象的類型或所屬類。當您使用鴨子類型時,你無需做檢查類型。相反,您要檢查給定方法或屬性是否存在。

鴨子類型語言為程序員提供了最大的靈活性,程序員只需寫最少量的代碼。但是這些語言可能並不安全,可能會產生運行時錯誤,使用時需要格外注意。

參考鏈接:

  1. Python術語表://docs.python.org/3/glossary.html#term-duck-typing
  2. 維基百科://zh.wikipedia.org/wiki/鴨子類型
Tags: