79、Python之鸭子类型:没有听过鸭子类型?关键在于认知的转变
引言
不同于Java等静态类型的语言,Python基于动态类型系统的设计理念,使得Python在很多应用场景中,显得更急灵活、高效。而在动态类型系统中,有一个很重要的概念,就是“鸭子类型”。鸭子类型的背后,代表的是一些编程认知方式的转变,是对“协议”、“行为”与类型之间关系的更加深入的理解。
本文的主要内容有:
1、什么是鸭子类型
2、简单对比Python与Java的类型系统
3、鸭子类型的应用
4、鸭子类型的注意事项
什么是鸭子类型
所谓“鸭子类型(Duck Typing)”,是一种动态类型系统的编程概念。其核心思想来自于詹姆斯·惠特科姆·莱利的鸭子测试:“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子”。
换句话说,在鸭子类型中,对象的有效性不依赖于其显式的类型,而是依赖于对象是否具有所需要的属性和方法。
可以粗略的理解为,所谓的实体的强类型,更像是一种标签。而我们在实际业务场景中,需要的是某些能力、行为。所以,只要某个实体对象,具备了我们所需要的能力,能够在系统交互中实现所需要的行为,那么至于这个实体对象被标记为什么类型,其实并不是很重要。
以“捉老鼠”的场景为例,我们最根本的诉求是不被老鼠所影响,想到的方法,或者需要具备的能力是“能够把老鼠捉起来”。至于是白猫、黑猫、多管闲事的狗,又或者是有些屠龙技的人,只要能达到捉到老鼠的目的即可。
所以,本质上来说,在动态类型系统中,我们所关注的不再是静态类型系统中,一个实体对象所属的类型,而是这个实体对象所具备的属性和方法。而所应该具备的属性和方法,更加抽象来说,叫做“协议”。所以,我们的关注视角,从具体类型转换到了协议。能理解了这一个认知的转换,就已经理解了鸭子类型的本质。
简单对比Python与Java的类型系统
如果有同学比较熟悉Java,应该能够深刻地感受到,在Java的编程世界中,类型系统是静态的,类型检查是在编译时进行的,并且对象的类型是显式声明的。这样的好处在于,有些错误可以在编译期就能提早发现。但是,也会导致更多的强制类型转换、一大堆样板代码等。
在Java中,如果我们定义不同动物,定义一个函数接收具体的动物对象,发出不同的叫声,我们大概需要这样做:
package com.study;
interface Animal {
void bark();
}
class Dog implements Animal {
@Override
public void bark() {
System.out.println("小狗汪汪汪");
}
}
class Duck implements Animal {
@Override
public void bark() {
System.out.println("鸭子嘎嘎嘎");
}
}
public class Zoo {
public static void make_sound(Animal animal) {
animal.bark();
}
public static void main(String[] args) {
Dog dog = new Dog();
make_sound(dog);
Duck duck = new Duck();
make_sound(duck);
}
}
需要先有一个表示动物的类型,可以是一个接口或者抽象类,用于指向具体的动物子类型的对象。
定义对象,还要明知道类型,再手动显式声明类型。
运行结果:
这是一套标准的面向对象的继承的设计实现,也确实达到了我们想要的效果。
在Python中,我们也可以通过面向对象的继承的思路,来实现同样的效果。但是,由于Python是动态类型的,我们还可以有更简化的实现方式:
class Dog:
def bark(self):
print('小狗汪汪汪')
class Duck:
def bark(self):
print('鸭子嘎嘎嘎')
def make_sound(animal):
animal.bark()
if __name__ == '__main__':
dog = Dog()
make_sound(dog)
duck = Duck()
make_sound(duck)
从代码行数的角度,依然能看到更加简洁。
从设计实现上,Dog和Duck两个类型没有任何继承关系(当然都是object类的子类,这点这里可以忽略)。它们定义了同样的方法bark(),所以在进行函数make_sound()调用时,只要传入的对象实例具有bark()方法,就能成功执行。
执行结果:
从上面的对比可以看出,Python中,类型的检查是运行时进行的,类型是隐式的。Python中的鸭子类型的存在,允许我们更加专注于对象的行为,而非其必须是某个特定类型,或者繁琐地进行类型继承关系的设计。
鸭子类型的应用
1、编程的灵活性和多态性
鸭子类型的引入,首先给我们提供了一种新的“多态”的思路,我们不需要进行类的继承体系的设计,不需要考虑抽象出具有特定功能的公共父类,只需要定义同样的行为方法即可。这种实现多态的方式,似乎是一种更加自然的方式。
2、代码的简洁性
鸭子类型减少了类型检查和类型转换的需求,从而使得代码更加简洁易读。在Java中,通常需要通过强制转换来实现多态行为。而在Python中,这些都变得不是必要的。
3、兼容性和扩展性
鸭子类型使得代码更加具有兼容性和可扩展性。之所以这样说,是因为,我们可以随时定义新的对象类型,只要定义特定的方法,就可以将新的类型的对象,传递给一个现有的函数。这样,使得对现有功能的扩展变得更加灵活、方便。
鸭子类型的注意事项
需要说明的是,虽然鸭子类型是我们接下来几篇文章的主角,但是还是有一些潜在的需要注意的事项:
1、运行时错误
由于类型检查被推迟到了运行时进行,所以,有些错误只有在实际运行代码时,才会被发现。所以,良好的测试变得更加重要。
2、代码的可读性
对于新手或者不熟悉代码库的开发者来说,鸭子类型在一定程度上会导致代码可读性的降低。所以,适当的注释和文档说明,也变得更加重要了。
总结
本文重点介绍了“鸭子类型”背后的核心理念,专注于行为而非类型本身。同时,对比了Java和Python这两种语言所代表的类型系统的差别,尤其在多态实现上的差异。尽管“鸭子类型”赋予了Python极大的灵活性和简洁性,但是,还是需要注意这些有点背后潜在的风险。
感谢您的拨冗阅读,希望对您有所帮助。