鸭子类型与(Java)接口概念

26 投票
4 回答
5447 浏览
提问于 2025-04-16 20:51

我刚刚读了维基百科关于鸭子类型的文章,感觉我对接口的概念理解得不太全面,特别是我以前在Java中用到的那种:

"When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck."


class Duck:
    def quack(self):
        print("Quaaaaaack!")
    def feathers(self):
        print("The duck has white and gray feathers.")
    def swim(self):
        print("Swim seamlessly in the water")

class Person:
    def quack(self):
        print("The person imitates a duck.")
    def feathers(self):
        print("The person takes a feather from the ground and shows it.")
    def name(self):
        print("John Smith")

def in_the_forest(duck):
    duck.quack()
    duck.feathers()

def game():
    donald = Duck()
    john = Person()
    in_the_forest(donald)
    in_the_forest(john)

game()

假设在in_the_forest里,我写了:

  • 它是不是像鸭子一样呱呱叫?是的
  • 它有没有鸭子的羽毛?有的
  • 太好了,我们有一只鸭子!

然后,因为我知道它是一只鸭子,我想让它游泳?结果john就会沉下去!

我不想我的应用程序在运行过程中因为约翰假装成鸭子而随机崩溃,但我想检查每一个对象的属性似乎也不是个好主意……?

4 个回答

5

我不想我的应用程序在运行过程中因为约翰假装成鸭子而随机崩溃,但我想检查每个对象的所有属性似乎也不是个好主意……?

这其实是动态类型的一种问题。比如在像Java这样的静态类型语言中,编译器会在编译时检查Person是否实现了IDuck接口。而在像Python这样的动态类型语言中,如果Person缺少某个特定的鸭子特征(比如swim),那么在运行时就会出现错误。引用维基百科的另一篇文章(“类型系统”,动态类型部分):

动态类型可能会导致运行时类型错误,也就是说,在运行时,一个值可能会有意想不到的类型,并且对该类型进行的操作可能是无意义的。这种错误可能会在编程错误发生很久之后才出现,也就是错误的数据类型被传递到不该去的地方。这可能会让找到这个bug变得很困难。

动态类型有它的缺点(你提到过一个)和优点。关于这两者的简要比较可以在维基百科的类型系统文章的另一部分找到:静态和动态类型检查的实践

5

我不能代表其他编程语言,但在Python中,最近(在版本2.6中)引入了一个叫做抽象基类(ABC)模块的东西。

如果你看看它被引入的原因(可以参考PEP 3119),你会很快明白,部分原因是为了“拯救约翰免于死亡”,换句话说,就是为了确保当你按照接口编程时,所有接口的方法都会存在。引用的PEP中提到:

抽象基类(ABC)实际上就是Python类,它们被添加到对象的继承树中,以向外部检查者表明该对象的某些特性。使用isinstance()函数进行测试,如果某个特定的ABC存在,就意味着测试通过。此外,ABC还定义了一组最基本的方法,这些方法确定了该类型的特征行为。基于ABC类型来区分对象的代码可以信任这些方法总是会存在。

一般来说,你可以在自己的代码中应用相同的模式。比如说:你可以创建一个BasePlugin类,里面包含插件工作所需的所有方法,然后通过继承这个类来创建几个不同的插件。根据每个插件是否必须可以定义那些方法,你可以让BasePlugin的方法默默通过(插件可以定义这些方法)或者抛出异常(插件必须定义这些方法/重写BasePlugin的方法)。

编辑:在下面的评论中,有人建议我在回答中加入这段讨论:

这种特性——至少在Python中——并不是为了人类程序员的方便(Python从来不会静默错误,所以已经有很多反馈了),而是为了Python自身的自省能力(这样可以更容易地编写动态加载、元编程代码等)。换句话说:我知道约翰不能飞……但我希望Python解释器也知道这一点! :)

26

鸭子类型并不是在检查你需要的东西是否存在,然后再去使用它。鸭子类型的意思是直接使用你需要的东西。

in_the_forest 这个函数是一个开发者写的,他在想鸭子。这个函数是为了处理一个 Duck(鸭子)而设计的。鸭子可以 quack(叫声)和有 feathers(羽毛),所以程序员用这些特性来完成任务。在这个例子中,鸭子还可以 swim(游泳),但这个特性没有被用到,也不需要。

在像 Java 这样的静态语言中,in_the_forest 会被声明为接受一个 Duck。当程序员后来发现他们有一个 Person(人),这个人也可以 quack 和有 feathers,想要重用这个函数时,就麻烦了。PersonDuck 的子类吗?显然不是。有没有 QuacksAndFeathers 接口?也许有,但不一定。如果没有,他们就得自己创建一个,还要修改 Duck 来实现这个接口,并把 in_the_forest 修改为接受 QuacksAndFeathers 而不是 Duck。如果 Duck 在外部库中,这可能会变得很麻烦。

而在 Python 中,你只需把 Person 传给 in_the_forest,它就能正常工作。因为实际上 in_the_forest 不需要一个真正的 Duck,它只需要一个“像鸭子一样”的对象,而在这个例子中,Person 足够像鸭子。

不过,game 需要一个更严格的“像鸭子一样”的定义。在这里,约翰·史密斯就不太幸运了。

确实,Java 会在编译时捕捉到这个错误,而 Python 则不会。这可以被看作是一个缺点。支持动态类型的人会反驳说,你写的任何大量代码总会包含一些没有编译器能捕捉到的错误(说实话,Java 也不是一个特别能捕捉很多错误的强静态检查编译器)。所以你需要测试你的代码来发现这些错误。如果你在测试这些错误时,自然会发现你把 Person 传给了一个需要 Duck 的函数。基于这个,支持动态类型的人会说,一个让你不去测试的语言,因为它能发现你的一些小错误,实际上是个坏事。而且,它还会阻止你做一些非常有用的事情,比如在 Person 上重用 in_the_forest 函数。

我个人在这两种观点中很矛盾。我非常喜欢 Python 的灵活动态类型。同时,我也很喜欢 Haskell 和 Mercury 的强大静态类型系统。我不太喜欢 Java 或 C++;在我看来,它们有静态类型的所有坏处,却几乎没有好处。

撰写回答