Python学习笔记——鸭子类型(duck typing)

前言

在实习期间,由于工作需要首次接触了Python这门语言,由于学习和使用的时间非常短,所以当时认为,作为一门解释性语言,在做Web开发方面,Python和PHP的差别不大,甚至在一些应用场景上没有PHP来的简单粗暴。后来,在导师的推荐下,通过《流畅的Python》又一次深入的学习了Python,大致从数据结构、函数、面向对象和控制流程这几个部分深入的学习了这一门语言,对其中作为一等公民的函数和面向对象的实现留下了深刻的映像,开始体会到这门语言的独有魅力。
这本书中,对于Python面向对象的实现机制,介绍了一个非常有趣的概念:鸭子类型(duck typing)。本文将基于鸭子类型这一概念来记录和分享一些Python的学习体会,同时结合过去对Java的学习,比较这两种风格截然不同的语言。同时,基于Java的面向对象,提出鸭子类型背后的两种面向对象思想:多态和范型

什么是鸭子类型

在维基百科中是这样定义的:

鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。

而鸭子类型这一名字出自James Whitcomb Riley在鸭子测试中提出的如下的表述:

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

简单归纳就是:对象的类型不再由继承等方式决定,而由实际运行时所表现出的具体行为来决定。
这个概念在解释性语言中还是非常容易理解的,因为解释性语言在定义函数的参数时是无法指定具体参数类型的,另一方面,参数的类型是在解释执行时才能确定,不像类Java语言一样,在编译期编译器就可以确定参数类型,从而在多态模式下确定函数的执行版本了。
另一方面,Python并未沿用Java语言中复杂的接口和类的继承框架,因此对于面向对象有独有的实现风格,接下来的内容和今后更新的博客将详细探讨这个问题。

一个鸭子类型的实例

存在这样一个应用场景:在电子商务系统中,华为旗舰店为了进行商品促销,设计了多套促销方案,而我们需要为该店实现价格计算功能。那么,这个功能可简单的抽象为一个函数:discount_compute(user, product, num),该函数主要由3个参数构成,用户信息user,商品信息product,购买数量num,同时为了实现促销,商品类还应分别实现两个功能:折扣促发条件condition函数和折扣方式get_discount函数。而这时,苹果旗舰店也要进行促销活动,显然苹果旗舰店不是华为旗舰店,但是在鸭子类型的编程模式下,苹果旗舰店只要根据协议,在对应商品类中实现condition函数get_discount函数就可以直接使用我们设计好的价格计算功能了。
哈哈,通过上面的举例描述,相信学过Java或C++的同学一眼就能看出,这不就是多态嘛。利用接口或者超类,将统一的行为进行抽象,再由具体的子类实现进行不同的功能扩展。没错,鸭子类型的编程风格,在实际的应用场景中,确实发挥的是一种面向对象中多态的功能。
Python作为一种解释性语言,相比于PHP的魅力也在这里,实现出一种自己独有的面向对象风格。下一章,将详细介绍Python中的鸭子风格的体现。

Python中的鸭子类型

首先,在Python中,面向对象的多态和抽象是通过把协议当作正式接口来实现的。关于这一点,最明显的特征是对于私密(private)属性Python没有具体的关键字进行支持,也就是说,在Python中,你定义的对象属性始终可以被修改。对此,Python社区只是通过提倡使用一种命名规则来声明私密变量,具体地,就是在变量前加上一个或两个_
因此,我们将在Python的很多特性中,不断看到协议这个概念,但是不同于Java,各种协议是没有具体的接口来进行支持的,甚至在Python中连接口申明的关键字都没有。接下来我们将详细的来讨论Python中的鸭子类型以及有关协议的实现。

a. 序列的实现

基本序列协议: 实现__len____getitem__方法
典型的,在Python中,如果希望使用序列的相关功能,例如通过索引进行随机访问list[index],使用一些Python提供的模板方法,例如序列上的排序方法sorted或者是以序列为参数的random.shuffle,最简单的方式都是实现序列协议,采用鸭子类型模式,能够以最低的开发成本使用这些功能。
例如,我们实现了一个简单的纸牌类FrenchDeck,现在需要能够添加洗牌功能,那么只需要让纸牌类实现序列协议即可,示例代码如下:

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]

若不使用序列协议,那么洗牌的功能,将是一个重复造轮子的过程,同时,如果处理不好随机化问题,还会带来新的风险和漏洞。
在这个示例中,纸牌类本身和序列没有任何关系,但是只要实现了序列协议,有了满足序列的相关行为,就成为了序列,因此,就可以正常和安全的使用相关内置方法。
也就是说,对于Python提供的很多以列表为参数的内置方法,我们都可以将实现了列表协议的不同类传递进行去,获对应功能。上面的描述可能有点绕,但是仔细一想,我们似乎又感受到另外一种面向对象的概念,那就是范型。没错,这不就正是范型的定义嘛,我们传递给方法的参数,没有继承相同的父类,甚至没有接口的概念,但是都能得到正确的处理。

b. 切片的实现

基本切片协议: 实现__getitem__方法
说到切片,这可是Python的一种高级使用方法,同时在其他的语言里面基本看不到的一种概念,我们可以通过切片功能,实现列表的灵活应用。
我们发现当实现了列表协议以后,已经能够正常使用分片了。在上述的纸牌例子中,我们已经可以正常使用分片的所有功能。
这里需要注意的是,在这个例子中,由于__getitem__方法是直接对内置类进行操作,那么可返回正常的对象,但是若操作的是序列本身,则需要进行特殊处理,具体示例如下:

class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)

    def __iter__(self):
        return iter(self._components)

    def __repr__(self):
        components = reprlib.repr(self._components)
        components = components[components.find('['):-1]
        return 'Vector({})'.format(components)

    def __len__(self):
        return len(self._components)

    def __getitem__(self, index):
        return self._components[index]

该示例的运行结果如下:

>>> from test1 import Vector
>>> v = Vector([1,2,3,4])
>>> v[:1]
array('d', [1.0])
>>> v[2:3]
array('d', [3.0])
>>> v[1:]
array('d', [2.0, 3.0, 4.0])

返回结果是array类型而非Vector类型,因此需要对__getitem__方法进行特殊处理,将返回结果统一为Vector类,可通过如下代码进行改进:

def __getitem__(self, index):
        cls = type(self)  
        if isinstance(index, slice):  
            return cls(self._components[index])  
        elif isinstance(index, numbers.Integral):  
            return self._components[index]  
        else:
            msg = '{cls.__name__} indices must be integers'
            raise TypeError(msg.format(cls=cls))  

c. 其他鸭子类型

Python中,通过对内置特殊函数的实现(前后双_函数),从而获得Python原生的支持,其中对一元中缀表达式的覆盖,对常规加法操作、乘法操作等的覆盖,皆是鸭子类型的表现形式。这些例子还有非常多,至此不再一一列举。

总结

此篇博客以鸭子类型这一编程风格为中心,探讨了Python的面向对象的部分特性,其中对鸭子类型的本质进行了分析和解释,更多的是自己对Python面向对象的一些理解。
同时,结合博主过去对Java面向对象的理解,讨论了Python面向对象的两大重要概念:多态和范型。同时结合《流畅的Python》中提供的两大经典示例进行了概念的分析。从而加深了对鸭子类型这一编程风格的理解,如若有不正之处,望各位能指出~
最后,今后本博客将继续更新:作为一等公民的函数、控制流程和元编程等章节,望与各位Python学习者一同成长,还望各位继续关注本博客,谢谢各位了?

你可能感兴趣的:(Python学习笔记)