Python 中的鴨子型別和猴子補丁

yongxinz發表於2022-04-11

原文連結: Python 中的鴨子型別和猴子補丁

大家好,我是老王。

Python 開發者可能都聽說過鴨子型別猴子補丁這兩個詞,即使沒聽過,也大概率寫過相關的程式碼,只不過並不瞭解其背後的技術要點是這兩個詞而已。

我最近在面試候選人的時候,也會問這兩個概念,很多人答的也並不是很好。但是當我向他們解釋完之後,普遍都會恍然大悟:“哦,是這個啊,我用過”。

所以,我決定來寫一篇文章,探討一下這兩個技術。

鴨子型別

引用維基百科中的一段解釋:

鴨子型別duck typing)在程式設計中是動態型別的一種風格。在這種風格中,一個物件有效的語義,不是由繼承自特定的類或實現特定的介面,而是由"當前方法和屬性的集合"決定。

更通俗一點的說:

當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。

也就是說,在鴨子型別中,關注點在於物件的行為,能作什麼;而不是關注物件所屬的型別。

我們看一個例子,更形象地展示一下:

# 這是一個鴨子(Duck)類
class Duck:
    def eat(self):
        print("A duck is eating...")

    def walk(self):
        print("A duck is walking...")


# 這是一個狗(Dog)類
class Dog:
    def eat(self):
        print("A dog is eating...")

    def walk(self):
        print("A dog is walking...")


def animal(obj):
    obj.eat()
    obj.walk()


if __name__ == '__main__':
    animal(Duck())
    animal(Dog())

程式輸出:

A duck is eating...
A duck is walking...
A dog is eating...
A dog is walking...

Python 是一門動態語言,沒有嚴格的型別檢查。只要 DuckDog 分別實現了 eatwalk 方法就可以直接呼叫。

再比如 list.extend() 方法,除了 list 之外,dicttuple 也可以呼叫,只要它是可迭代的就都可以呼叫。

看過上例之後,應該對「物件的行為」和「物件所屬的型別」有更深的體會了吧。

再擴充套件一點,其實鴨子型別和介面挺像的,只不過沒有顯式定義任何介面。

比如用 Go 語言來實現鴨子型別,程式碼是這樣的:

package main

import "fmt"

// 定義介面,包含 Eat 方法
type Duck interface {
	Eat()
}

// 定義 Cat 結構體,並實現 Eat 方法
type Cat struct{}

func (c *Cat) Eat() {
	fmt.Println("cat eat")
}

// 定義 Dog 結構體,並實現 Eat 方法
type Dog struct{}

func (d *Dog) Eat() {
	fmt.Println("dog eat")
}

func main() {
	var c Duck = &Cat{}
	c.Eat()

	var d Duck = &Dog{}
	d.Eat()

	s := []Duck{
		&Cat{},
		&Dog{},
	}
	for _, n := range s {
		n.Eat()
	}
}

通過顯式定義一個 Duck 介面,每個結構體實現介面中的方法來實現。

猴子補丁

猴子補丁Monkey Patch)的名聲不太好,因為它會在執行時動態修改模組、類或函式,通常是新增功能或修正缺陷。

猴子補丁在記憶體中發揮作用,不會修改原始碼,因此只對當前執行的程式例項有效。

但如果濫用的話,會導致系統難以理解和維護。

主要有兩個問題:

  1. 補丁會破壞封裝,通常與目標緊密耦合,因此很脆弱
  2. 打了補丁的兩個庫可能相互牽絆,因為第二個庫可能會撤銷第一個庫的補丁

所以,它被視為臨時的變通方案,不是整合程式碼的推薦方式。

按照慣例,還是舉個例子來說明:

# 定義一個Dog類
class Dog:
    def eat(self):
        print("A dog is eating ...")


# 在類的外部給 Dog 類新增猴子補丁
def walk(self):
    print("A dog is walking ...")


Dog.walk = walk

# 呼叫方式與類的內部定義的屬性和方法一樣
dog = Dog()
dog.eat()
dog.walk()

程式輸出:

A dog is eating ...
A dog is walking ...

這裡相當於在類的外部給 Dog 類增加了一個 walk 方法,而呼叫方式與類的內部定義的屬性和方法一樣。

再舉一個比較實用的例子,比如我們常用的 json 標準庫,如果說想用效能更高的 ujson 代替的話,那勢必需要將每個檔案的引入:

import json

改成:

import ujson as json

如果這樣改起來成本就比較高了。這個時候就可以考慮使用猴子補丁,只需要在程式入口加上:

import json  
import ujson  


def monkey_patch_json():  
    json.__name__ = 'ujson'  
    json.dumps = ujson.dumps  
    json.loads = ujson.loads  


monkey_patch_json()

這樣在以後呼叫 dumpsloads 方法的時候就是呼叫的 ujson 包,還是很方便的。

但猴子補丁就是一把雙刃劍,問題也在上文中提到了,看需,謹慎使用吧。

以上就是本文的全部內容,如果覺得還不錯的話,歡迎點贊轉發關注,感謝支援。


推薦閱讀:

相關文章