简单聊聊Python中的鸭子类型和猴子补丁
作者:yongxinz 发布时间:2022-06-17 00:32:58
前言
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 是一门动态语言,没有严格的类型检查。只要 Duck 和 Dog 分别实现了 eat 和 walk 方法就可以直接调用。
再比如 list.extend() 方法,除了 list 之外,dict 和 tuple 也可以调用,只要它是可迭代的就都可以调用。
看过上例之后,应该对「对象的行为」和「对象所属的类型」有更深的体会了吧。
再扩展一点,其实鸭子类型和接口挺像的,只不过没有显式定义任何接口。
比如用 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)的名声不太好,因为它会在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷。
猴子补丁在内存中发挥作用,不会修改源码,因此只对当前运行的程序实例有效。
但如果滥用的话,会导致系统难以理解和维护。
主要有两个问题:
补丁会破坏封装,通常与目标紧密耦合,因此很脆弱
打了补丁的两个库可能相互牵绊,因为第二个库可能会撤销第一个库的补丁
所以,它被视为临时的变通方案,不是集成代码的推荐方式。
按照惯例,还是举个例子来说明:
# 定义一个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()
这样在以后调用 dumps 和 loads 方法的时候就是调用的 ujson 包,还是很方便的。
但猴子补丁就是一把 * 剑,问题也在上文中提到了,看需,谨慎使用吧。
来源:https://mp.weixin.qq.com/s/3WGFkl9MRbYjojFK7-eEww


猜你喜欢
- 触发器是一种特殊类型的存储过程,它不同于之前的我们介绍的存储过程。触发器主要是通过事件进行触发被自动调用执行的。而存储过程可以通过存储过程的
- mysql日期相减的天数函数DATEDIFF() 函数返回两个日期之间的天数。语法DATEDIFF(date1,date2)date1 和
- 我就废话不多说了,大家还是直接看代码吧~type Wait interface { // Register waits returns a
- 导语"? 花草树木 皆有呈名热爱自然,从认识自然开始 "现在的植物爱好者,遇到不认得的植物。怎么办呢?前几天去逛商场,一
- 在用django写项目时,遇到了许多场景,关于ORM操作获取数据的,但是不好描述出来,百度搜索关键词都不知道该怎么搜,导致一个人鼓捣了好久。
- ASP实例代码,利用SQL语句动态创建Access表。留作参考,对在线升级数据库有用处.<% nowtime = now()
- 1. 参数解析1.1 inplace参数取值:True、FalseTrue:直接修改原对象False:创建一个副本,修改副本,原对象不变(缺
- 我们可能会出现这种情况,某个表原来设计不周全,导致表里面的数据数据重复,那么,如何对重复的数据进行删除呢?重复的数据可能有这样两种情况,第一
- 目录前言示例文件文件编码空值日期错误函数映射方法1:直接使用labmda表达式方法二:使用自定义函数方法三:使用数值字典映射总结前言本文是给
- 1、下载从如下地址下载mycat的安装包:http://www.mycat.io/2、解压解压下载的安装包3、安装安装mycat
- 目录mysql主从复制mysql主从复制的方式mysql主从复制的原理mysql的主从配置的具体实现方式1、 Master配置2、 Slav
- 官网地址:https://www.mysql.com/安装建议:尽量不要用.exe进行安装,用压缩包安装,对日后的卸载更为方便下载地址:ht
- with语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下问题。这样做能避免错误并减少样板代码,因此API能更安全,更易
- 本文实例讲述了PHP开发之归档格式phar文件概念与用法。分享给大家供大家参考,具体如下:一个php应用程序往往是由多个文件构成的,如果能把
- 本文实例讲述了C语言实现访问及查询MySQL数据库的方法。分享给大家供大家参考,具体如下:1、添加头文件路径(MySQL安装路径中的incl
- 核心代码# -*- coding: utf-8 -*-'''python读取英文文件,将每个单词按照空格分开,并将每
- 本文实例讲述了thinkPHP框架实现类似java过滤器的简单方法。分享给大家供大家参考,具体如下:写java web代码的时候,可以定义过
- 目的: 从数据库读取二进制位图图形数据资料, 透过 ImageMagickObject 组件即时制作缩略图,并显示在网页上 (ge
- 本文为大家分享了如何用Navicat把csv数据导入mysql,供大家参考,具体内容如下1.获取csv数据,用office另存为功能把exc
- 这里想象一下需求,写一个项目使用的一系列1.0版本的插件,现在要新写一个项目,需要用这些插件的2.0版本,该怎么办?都更新成2.0版本?这样