目录

鸭子类型

前言

在学习 Python 的过程中,你一定会听说过这个名词:鸭子类型(duck typing)。关于鸭子类型最有名的一句话是:

“当你看到一只鸟走起来像鸭子、游起泳来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

这句话来源于詹姆斯·惠特科姆·莱利提出的鸭子测试。现在许多 Python 的教科书上都会提到这个概念,但有些书上并没有展开介绍细节。

本文就通过一些实例来探究一下什么是鸭子类型。

注意

本文中的命令行示例,都是基于以下环境:

  • 操作系统: CentOS 7
  • Python:3.7.9
  • GCC:4.8.5

 

多态性

在理解鸭子类型之前,我们先要了解一下什么是多态性。概括地说,多态性(polymorphism)表示为”相同的接口,不同的实现“,或”一个接口,多种方法“。

多态性一般又分为编译时多态性运行时多态性。以 C++ 为例,编译时多态性通过函数重载实现,在编译环节实现早期绑定(early binding);而运行时多态性通过虚函数实现,在运行环节实现延迟绑定(late binding),也称为动态绑定(dynamic binding)

运行时多态性,或者动态绑定,就是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为。它是面向对象编程的一个重要概念。本文后面提到的多态性,默认指运行时多态性。

以 C++ 为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

class Animal
{
public:
  virtual void shout()
  {
    std::cout << "I'm an animal." << std::endl;
  }
};


class Duck: public Animal
{
public:
  virtual void shout()
  {
    std::cout << "I'm a duck." << std::endl;
  }
};

void f(Animal *animal)
{
  std::cout << "who are you?" << std::endl;
  animal->shout();
}


int main()
{
  Duck *duck = new Duck();
  f(duck);
  return 0;
}

执行结果:

1
2
who are you?
I'm a duck.

可以看到,虽然函数 f 的形参是 Animal 类型的指针,但实际使用中,我们可以把任何 Animal 子类类型的指针传递给 f 函数。而在函数内部,当我们通过 Animal 指针来调用 shout() 方法的时候,实际调用的是具体对象所属类型的方法,而不是基类的方法。

编程语言中的类型系统 一文中本人介绍过,静态类型语言,或多或少也会做一些动态类型检查,一个典型的例子是C++ 运行时类型检查(C++ RTTI)。C++ 动态类型检查是为了实现动态绑定,就像这个例子演示的那样,一个基类类型的指针或引用,运行时指向的对象可以是基类对象本身,也可以子类的对象,而通过该指针或引用来访问方法的时候,会根据实际指向的对象类型来调用对应的方法。

 

Python 中的多态性

上面介绍了在静态类型语言 C++ 中实现多态性,那么在 Python 中如何实现呢?

Python 中实现多态性,要比 C++ 简单,上一个例子,用 Python 实现如下;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Animal(object):
  def shout(self):
    print("I'm an animal.")

class Duck(Animal):
  def shout(self):
    print("I'm a duck.")

def f(animal):
  print("who are you?")
  animal.shout()

duck = Duck()
f(duck)

执行结果:

1
2
who are you?
I'm a duck.

Python 的实现比 C++ 简单,一方面因为 Python 语法简单,另一方面因为 Python 是动态类型语言,声明变量、形参等不需要指定类型,当调用一个对象的方法的时候,会根据对象的实际类型来调用对应的方法。

如果说 C++ 的多态性是靠 C++ RTTI 实现了动态绑定,Python 的多态性就完全是“天生”的,Python 的动态类型检查使得多态性在面向对象程序中体现得淋漓尽致。其实在 Python 中,即便不通过类的继承,也能实现动态绑定。这个方法就是鸭子类型。

 

什么是鸭子类型

鸭子类型(duck typing)在程序设计中是动态类型的一种风格。回到关于鸭子类型的那句名言:当你看到一只鸟走起来像鸭子、游起泳来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。中翻中,这句话的意思就是到底是鸭子还是鸟,要看它的行为,而不是类型。换句话说,在鸭子类型中,关注点在于对象的行为,而不是对象所属的类型。

在不使用鸭子类型的语言中,我们编写一个函数,它只能接受一个类型为"鸭子"的对象,并调用它的“叫”方法。在使用鸭子类型的语言中,编写一个函数可以接受一个任意类型的对象,并调用它的“叫”方法。如果这个对象不支持“叫”方法,那么将引发一个运行时错误。任何拥有这样的“叫”方法的对象都可被函数接受,这种决定类型的方式因此得名。

我们把上一节的例子稍作改动:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal(object):
  def shout(self):
    print("I'm an animal.")

class Duck(Animal):
  def shout(self):
    print("I'm a duck.")

class Cat():
  def shout(self):
    print("I'm a cat.")

def f(animal):
  print("who are you?")
  animal.shout()

animal = Animal()
duck = Duck()
cat = Cat()


f(animal)
f(duck)
f(cat)

执行结果:

1
2
3
4
5
6
who are you?
I'm an animal.
who are you?
I'm a duck.
who are you?
I'm a cat.

我们新增了一个类 Cat,定义了它的 shout() 方法。这个类并没有继承自 Animal 父类,但我们依然可以创建一个 Cat 对象,并把它传给 f 函数,在 f 函数内成功调用它的 shout() 方法。

任何对象,只要它有 shout() 方法,哪怕这个对象是一个模块,都可以传给 f 函数并成功执行。这就是鸭子类型的本意。

上述例子,用 C++ 的多态性是无法实现的,因为 C++ 多态性建立在类的继承上,它要求 f 函数的实参是形参 Animal 的子类,这里 Cat 类并不是Animal 的子类,所以编译时直接报错。

 

参考

https://en.wikipedia.org/wiki/Duck_typing


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」