这次跟大家分享编程领域里一个非常有趣的概念:“鸭子类型”。看到标题,相信有些朋友可能会疑问:程序语言跟“鸭子”有毛线关系?
『喜欢本篇请不吝点赞,感兴趣后续文章请关注我、关注我的专栏,谢谢您的支持鸭』
查看百科可以发现,这个概念竟然是来源于 美国印第安纳州的诗人詹姆斯·惠特科姆·莱利(James Whitcomb Riley,1849-1916)的诗句,原话翻译过来是这样讲的:
『当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。』
哈哈,听起来似乎“狗屁不通”,但这是一位名叫Alex Martelli的意大利软件工程师(同时也是Python软件基金会研究员),在2000年左右最早将这个概念引入到程序设计范畴中,之后这个概念在现代编程语言中被广泛推广。
那“鸭子类型”(Duck Typing,一种类型推断风格)在程序语言中到底是什么样的,又有怎样的意义呢?
要把这个概念讲得简单又有趣有一定的难度。笔者借用《Go语言编程》一书“接口查询”中提到的“医生”的例子,试着把它重新改编一下来讲。类比程序语言中的情景,假设在运行的列车上,有个人受伤了,现在需要一个能为他包扎伤口的人,可能是一个医生或护士角色的人,为了确认这个人的能力,可能会提出这样的问句:
『你是医生吗?』
大多数面向对象的编程语言,大概会把“你是医生吗”理解为类似这样的提问形式,如“你爸爸是医生吗?”(父类继承的方法,“祖传技艺”),“你有医生执照吗?”(预先实现的接口)。而在Go和Python这里,是理解为“你会包扎伤口吗?”(根据协议或条件推断)这样的形式的。
本文着重关注Go与Python这两门程序语言。笔者是在Python中第一次接触到“鸭子类型”的,在学习Go语言的过程中再次邂逅了“鸭子类型”。“鸭子类型”本身并不是跟编程语言耦合性特别大的概念,两种语言对待“鸭子”的思路是非常不一样的,语言本身上都分别针对它做了一些特别的“优化”。
概括地说:
- Python中主要是将“鸭子类型”的概念“常识化”(称之为“协议”,即在Python中是“约定俗成”的)
- Go将“鸭子类型”重新包装,认为在使用“鸭子”的时候根据使用需求,确定它是否有需要的“技能”(如鸭叫、游水、走路,等等),而不用确定它是不是真正的“鸭子”
(1)Python中的“鸭子类型”
Python具有简洁和容易上手的特点一定是有原因的。
《流畅的Python》一书中提到,Python实现“鸭子类型”,其中一种方式是通过“协议”这个概念,协议是“非正式的接口”。Python中并没有接口定义这种东西,对“面向对象”概念有一定认识的朋友可能会提出异议,没有“接口”的话很不好用啊。实际在学习实践Python的过程中,笔者的感觉是“真香”,其根本原因是因为Python把“接口”这种概念内置并且透明化了。
“你是医生吗?”的例子,在Python这里相当于问“你会做包扎伤口的操作吗?”,这样说可能不能够准确表达那种“协议”的概念。我们举个更简单的例子,小学的数学知识已经让我们懂得1 + 1是怎么加起来等于2了。作为一门比较标准的程序语言,Python它当然也能计算出1 + 1是等于2的,跟其他语言不大一样的地方在于,它传达给我们一种这样的思路:
『既然你懂得1 + 1之间加号的概念,那我给你定义X + Y的加号意义的自由。』
在Python中,经过开发者的定义,加减乘除的常见概念是可以变幻出无数种实际意义的。比如你可以定义 一匹公马“加”一匹母驴 等于一匹骡,扯淡一点的比如 一头猪“加”一只羊 等于一只“猪羊”(大雾)。这种能定义运算规则的方式(运算符重载),在Python中被归为“魔术方法”(Magic Method,或称Special Method),定义运算符意义只是其中的一类方法而已,Python通过魔术方法,把这种常识性的概念(这里的“常识性”也包括程序员对不同编程语言共有概念的印象,比如对构造方法、数组和索引等等的印象)推广到程序语言本身的多个方面中。
在实际使用中,笔者觉得这样的设计思想带来的好处是革命性的。比如机器学习中使用频率超高的Numpy和Pandas模块,将Python的“魔术方法”运用到了极致灵活的程度,通过我们熟知的运算符以及非常简洁的程序语句来调用他们封装好的方法,甚至有时不用看文档资料“试试就知道怎么用”,这样能把注意力更多集中到数据身上,而不是在入门还不熟悉的时候写两句查一个文档,显得特别不“Pythonic”,哈哈。
虽然不是只有Python语言有“运算符重载”这种操作,但Python的方式让笔者感觉特别地简洁有力高效。
我们顺带来看看Java语言,它很直接,直接没有“运算符重载”。拿《Head First 设计模式》一书中“用模板方法排序”来举例,如果要用Java给排排站的一列鸭子进行排序,通常的做法是先使用Java的列表(List)把这一列鸭子都存放起来,然后改写Java语言设计者实现定义好提供给我们的“模板方法”sort()和compareTo(),告诉Java鸭子应该怎样进行排序,请留意这个是在基于实现Collection接口的List类上进行实现的,听起来挺繁琐的对吧。
我们再来看看在Python中是怎么做的。Python的设计者根据经验认为“排序”在程序开发中是常见概念,也是相当常用的,于是先把排序定义为一个约定俗成的方法(内置sorted()函数),调用它时先看看被排序的鸭子是不是排成一列的状态(鸭子是否放在可迭代类型、有顺序概念的容器中),然后不用多说,让程序自己排就行了,万一程序不知道鸭子怎么排,那就自定义一个函数告诉它就可以了。同样的需求,Python实际写出来的代码要比Java代码简洁有力很多。
(2)Go中的“鸭子类型”
Go语言是一门在语言层面上支持“鸭子类型”的神奇的静态语言。同行们提到Go语言通常会吹爆它的goroutine,通常也会吐槽它的代码“写起来很恶心”。笔者开始学习Go语言,是被《Go语言四十二章经》教程中的一段话吸引到了:
『Go语言以语法简单、门槛低、上手快著称。但入门后很多人发现要写出地道的、遵循 Go语言思维的代码却是不易。
在刚开始学习中,我带着比较强的面向对象编程思维惯性来写代码。但后来发现,带着面向对象的思路来写Go 语言代码会很难继续写下去……刻意追求面向对象,会导致你很难理解接口在Go语言中的妙处。
……
网上有说Go大神的标准是“能理解简洁和可组合性哲学”,的确Go语言追求代码简洁到极致,而组合思想可谓借助于struct和interface两者而成为Go的灵魂。』
笔者的理解是,越简洁、越容易上手的程序语言,越难把它写得漂亮。而Go它像Python一样,不仅仅是有一定的简洁性,它还有一些概念上的革命性变化,以至于比Python更难“写得漂亮”。
我们通过《Go语言编程》 一书“其他语言的接口”中提到的“困扰了无数开发人员的传统难题”来引出讨论:
『问题1:我提供哪些接口好呢?
问题2:如果两个类实现了相同的接口,应该把接口放到哪个包好呢?』
回归到开篇“你是医生吗?”的例子中来。如果作为“需求方”的我们是需要考察这个医生有没有执照,那对于“提供方”的医生本身来说,问题1好比是一位医生在成为职业医生之前,“预先思考”应该考取覆盖了不同应用范围的哪些执照,问题2好比是设计执照证书的人,定义执照时“预先思考”执照的应用范围。这样的比方听起来好像不是很准确,笔者在此主要是想表达一个关键问题:“预先思考”很可能会脱离实际的用户应用场景。
Go语言提出了“非侵入式接口”的概念,“看似只是做了很小的文法调整,实则影响深远”(引自《Go语言编程》)。
我们还是按照 “你是医生吗?”这个例子来解释Go语言的这个概念。Go在提出确认医生问题时完全是换了一种角度来看待,它并不关心这个人是医生还是厨师,也不关心有没有医生执照,它从需求方只需要“包扎伤口”这一点来考虑,直接问这个人“你会包扎伤口吗?”。这样的比方在实际生活中听起来有点“扯淡”,但能呼应开篇提到的“鸭子类型”这一点,我们认定鸭子是根据鸭子的行为来认定的,而认定这个人“是否医生”在这个例子中显得并不重要了,只需要有“包扎伤口”的技能他就能像医生那样为人包扎伤口。
Go语言摒弃了传统的“面向对象”的思想,通过上述方式把程序代码结构打散,将原本需要提供方预先约定的接口变换为在使用时确定接口,在保证代码灵活性的同时又能keep住静态语言的各种优势,极大程度降低了各个模块间的耦合性,降低开发维护成本。
不仅仅是“非侵入式接口”的概念,在进一步了解Go语言的过程中,结合Go语言的其他优秀特性,笔者深深为其设计哲学折服。引用《Go语言编程》的这句话:
『Go语言在编程哲学上是变革派,而不是改良派。』
平时开玩笑时会说“从学习编程到学习哲学”。尽管Go语言代码对笔者来说目前可能并没有派上用场,或许以后可能也完全用不上,但笔者从直观感受上来说,Go语言拓宽了编程思维,不论是在写Python还是其他语言的代码上,还是在学习新出现的程序语言上,都是对自己有一定的帮助的。
(网络图片,来源于知乎问答《普通的程序员和大神级的程序员有什么区别?》)
特别感谢:
我的理解能力超强的老婆虽然是非IT专业,但在对“鸭子类型”的讨论中她给我非常大的启发,我感到很幸福~
『喜欢本篇请不吝点赞,感兴趣后续文章请关注我、关注我的专栏,谢谢您的支持鸭』