|
| 1 | +**原文链接:** [Python 中的鸭子类型和猴子补丁](https://mp.weixin.qq.com/s/3WGFkl9MRbYjojFK7-eEww) |
| 2 | + |
| 3 | +大家好,我是老王。 |
| 4 | + |
| 5 | +Python 开发者可能都听说过**鸭子类型**和**猴子补丁**这两个词,即使没听过,也大概率写过相关的代码,只不过并不了解其背后的技术要点是这两个词而已。 |
| 6 | + |
| 7 | +我最近在面试候选人的时候,也会问这两个概念,很多人答的也并不是很好。但是当我向他们解释完之后,普遍都会恍然大悟:“哦,是这个啊,我用过”。 |
| 8 | + |
| 9 | +所以,我决定来写一篇文章,探讨一下这两个技术。 |
| 10 | + |
| 11 | +## 鸭子类型 |
| 12 | + |
| 13 | +引用维基百科中的一段解释: |
| 14 | + |
| 15 | +> **鸭子类型**(**duck typing**)在程序设计中是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。 |
| 16 | +
|
| 17 | +更通俗一点的说: |
| 18 | + |
| 19 | +> 当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。 |
| 20 | +
|
| 21 | +也就是说,在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。 |
| 22 | + |
| 23 | +我们看一个例子,更形象地展示一下: |
| 24 | + |
| 25 | +```python |
| 26 | +# 这是一个鸭子(Duck)类 |
| 27 | +class Duck: |
| 28 | + def eat(self): |
| 29 | + print("A duck is eating...") |
| 30 | + |
| 31 | + def walk(self): |
| 32 | + print("A duck is walking...") |
| 33 | + |
| 34 | + |
| 35 | +# 这是一个狗(Dog)类 |
| 36 | +class Dog: |
| 37 | + def eat(self): |
| 38 | + print("A dog is eating...") |
| 39 | + |
| 40 | + def walk(self): |
| 41 | + print("A dog is walking...") |
| 42 | + |
| 43 | + |
| 44 | +def animal(obj): |
| 45 | + obj.eat() |
| 46 | + obj.walk() |
| 47 | + |
| 48 | + |
| 49 | +if __name__ == '__main__': |
| 50 | + animal(Duck()) |
| 51 | + animal(Dog()) |
| 52 | +``` |
| 53 | + |
| 54 | +程序输出: |
| 55 | + |
| 56 | +``` |
| 57 | +A duck is eating... |
| 58 | +A duck is walking... |
| 59 | +A dog is eating... |
| 60 | +A dog is walking... |
| 61 | +``` |
| 62 | + |
| 63 | +Python 是一门动态语言,没有严格的类型检查。只要 `Duck` 和 `Dog` 分别实现了 `eat` 和 `walk` 方法就可以直接调用。 |
| 64 | + |
| 65 | +再比如 `list.extend()` 方法,除了 `list` 之外,`dict` 和 `tuple` 也可以调用,只要它是可迭代的就都可以调用。 |
| 66 | + |
| 67 | +看过上例之后,应该对「**对象的行为**」和「**对象所属的类型**」有更深的体会了吧。 |
| 68 | + |
| 69 | +再扩展一点,其实鸭子类型和接口挺像的,只不过没有显式定义任何接口。 |
| 70 | + |
| 71 | +比如用 Go 语言来实现鸭子类型,代码是这样的: |
| 72 | + |
| 73 | +```go |
| 74 | +package main |
| 75 | + |
| 76 | +import "fmt" |
| 77 | + |
| 78 | +// 定义接口,包含 Eat 方法 |
| 79 | +type Duck interface { |
| 80 | + Eat() |
| 81 | +} |
| 82 | + |
| 83 | +// 定义 Cat 结构体,并实现 Eat 方法 |
| 84 | +type Cat struct{} |
| 85 | + |
| 86 | +func (c *Cat) Eat() { |
| 87 | + fmt.Println("cat eat") |
| 88 | +} |
| 89 | + |
| 90 | +// 定义 Dog 结构体,并实现 Eat 方法 |
| 91 | +type Dog struct{} |
| 92 | + |
| 93 | +func (d *Dog) Eat() { |
| 94 | + fmt.Println("dog eat") |
| 95 | +} |
| 96 | + |
| 97 | +func main() { |
| 98 | + var c Duck = &Cat{} |
| 99 | + c.Eat() |
| 100 | + |
| 101 | + var d Duck = &Dog{} |
| 102 | + d.Eat() |
| 103 | + |
| 104 | + s := []Duck{ |
| 105 | + &Cat{}, |
| 106 | + &Dog{}, |
| 107 | + } |
| 108 | + for _, n := range s { |
| 109 | + n.Eat() |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +通过显式定义一个 `Duck` 接口,每个结构体实现接口中的方法来实现。 |
| 115 | + |
| 116 | +## 猴子补丁 |
| 117 | + |
| 118 | +**猴子补丁**(**Monkey Patch**)的名声不太好,因为它会在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷。 |
| 119 | + |
| 120 | +猴子补丁在内存中发挥作用,不会修改源码,因此只对当前运行的程序实例有效。 |
| 121 | + |
| 122 | +但如果滥用的话,会导致系统难以理解和维护。 |
| 123 | + |
| 124 | +主要有两个问题: |
| 125 | + |
| 126 | +1. 补丁会破坏封装,通常与目标紧密耦合,因此很脆弱 |
| 127 | +2. 打了补丁的两个库可能相互牵绊,因为第二个库可能会撤销第一个库的补丁 |
| 128 | + |
| 129 | +所以,它被视为临时的变通方案,不是集成代码的推荐方式。 |
| 130 | + |
| 131 | +按照惯例,还是举个例子来说明: |
| 132 | + |
| 133 | +```python |
| 134 | +# 定义一个Dog类 |
| 135 | +class Dog: |
| 136 | + def eat(self): |
| 137 | + print("A dog is eating ...") |
| 138 | + |
| 139 | + |
| 140 | +# 在类的外部给 Dog 类添加猴子补丁 |
| 141 | +def walk(self): |
| 142 | + print("A dog is walking ...") |
| 143 | + |
| 144 | + |
| 145 | +Dog.walk = walk |
| 146 | + |
| 147 | +# 调用方式与类的内部定义的属性和方法一样 |
| 148 | +dog = Dog() |
| 149 | +dog.eat() |
| 150 | +dog.walk() |
| 151 | +``` |
| 152 | + |
| 153 | +程序输出: |
| 154 | + |
| 155 | +``` |
| 156 | +A dog is eating ... |
| 157 | +A dog is walking ... |
| 158 | +``` |
| 159 | + |
| 160 | +这里相当于在类的外部给 `Dog` 类增加了一个 `walk` 方法,而调用方式与类的内部定义的属性和方法一样。 |
| 161 | + |
| 162 | +再举一个比较实用的例子,比如我们常用的 `json` 标准库,如果说想用性能更高的 `ujson` 代替的话,那势必需要将每个文件的引入: |
| 163 | + |
| 164 | +```python |
| 165 | +import json |
| 166 | +``` |
| 167 | + |
| 168 | +改成: |
| 169 | + |
| 170 | +```python |
| 171 | +import ujson as json |
| 172 | +``` |
| 173 | + |
| 174 | +如果这样改起来成本就比较高了。这个时候就可以考虑使用猴子补丁,只需要在程序入口加上: |
| 175 | + |
| 176 | +```python |
| 177 | +import json |
| 178 | +import ujson |
| 179 | + |
| 180 | + |
| 181 | +def monkey_patch_json(): |
| 182 | + json.__name__ = 'ujson' |
| 183 | + json.dumps = ujson.dumps |
| 184 | + json.loads = ujson.loads |
| 185 | + |
| 186 | + |
| 187 | +monkey_patch_json() |
| 188 | +``` |
| 189 | + |
| 190 | +这样在以后调用 `dumps` 和 `loads` 方法的时候就是调用的 `ujson` 包,还是很方便的。 |
| 191 | + |
| 192 | +但猴子补丁就是一把双刃剑,问题也在上文中提到了,看需,谨慎使用吧。 |
| 193 | + |
| 194 | +以上就是本文的全部内容,如果觉得还不错的话,欢迎**点赞**,**转发**和**关注**,感谢支持。 |
| 195 | + |
| 196 | +--- |
| 197 | + |
| 198 | +**推荐阅读:** |
| 199 | + |
| 200 | +- [**Python 学习路线(2022)**](https://mp.weixin.qq.com/s/CyJ92-CD1xnihlp-Dqj8Yw) |
| 201 | +- [我写的 Python 代码,同事都说好](https://mp.weixin.qq.com/s/shO7Vw8U3xEJelzXgCa_mQ) |
0 commit comments