Python鸭子类型 duck typing

鸭子类型(Duck Typing)

在翻看fluent python这本书的时候看到第11章中讲到的 “从鸭子类型的代表特征动态协议,到使接口更明确、能验证实现是否符合规定的抽象基类(Abstract Base Class, ABC)。” 因此特意搜索了一下什么是鸭子类型。

定义

Duck Typing 概念来源于美国印第安纳州的诗人詹姆斯·惠特科姆·莱利(James Whitcomb Riley,1849-
1916)的诗句:”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.” 中文直译:“当看到一只鸟走路像鸭子,游泳像鸭子,叫声像鸭子,那么我称这只鸟为鸭子。” 言外之意,我们并不关心对象是什么类型,到底是不是鸭子,我们只关心它的行为。

In Python,也有很多不关心其本体,只关心其行为的东西。例如,StringIO和GzipFile,有很多相同的方法,我们把它们当做文件来使用。相关文档见如下:

  • https://docs.python.org/3.8/library/io.html?highlight=stringio#io.StringIO
  • https://docs.python.org/3/library/gzip.html

又如list.extend()传入的参数可以是list,可以是tuple,也可以是set,从syntax的描述中可以看到,extend传入的参数可以是任意iterable。因为它不关心传入的具体是什么,只关心传入的东西是否具有iterate的能力。
又如list.extend()传入的参数可以是list,可以是tuple,也可以是set,从syntax的描述中可以看到,extend传入的参数可以是任意iterable。因为它不关心传入的具体是什么,只关心传入的东西是否具有iterate的能力。
最后用一句话来wrap up鸭子类型的定义:鸭子类型是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类,或实现特定的接口,只要具备特定的属性或方法,能通过鸭子测试,即可使用。

代码样例

code & expected output

def calculate(a,b,c):
	return (a+b)*c

example1 = calculate(1,2,3)
example2 = calculate ([1, 2, 3], [4, 5, 6], 2)
example3 = calculate ('apples ', 'and oranges, ', 3)

print(example1)
print(example2)
print(example3)

outputs:
-------
9
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
apples and oranges, apples and oranges, apples and oranges, 

Criticism

摘自Wiki:

关于鸭子类型常常被引用的一个批评是它要求程序员在任何时候都必须很好地理解他/她正在编写的代码。在一个强静态类型的、使用了类型继承树和参数类型检查的语言中,给一个类提供未预测的对象类型更为困难。例如,在Python中,你可以创建一个称为Wine的类,并在其中需要实现press方法。然而,一个称为Trousers的类可能也实现press()方法。为了避免奇怪的、难以检测的错误,开发者在使用鸭子类型时需要意识到每一个“press”方法的可能使用,即使在语义上和他/她所正在编写工作的代码没有任何关系。

本质上,问题是:“如果它走起来像鸭子并且叫起来像鸭子”,它也可以是一只正在模仿鸭子的龙。尽管它们可以模仿鸭子,但也许你不总是想让龙进入池塘。

How to Handle it in Python?

对于duck typing潜在的风险,可以通过LBYL原则和EAFP原则来避免。前者是no pythonic way,而后者是pythonic way,属于Python中较为提倡且优雅的写法。

LBYL即 Look Before You Leap,是在进行相关操作前,预先进行检查。典型例子是使用if… else…

EAFP则是 Easier to Ask for Forgiveness than Permission,即操作前不做检查,出现error由异常处理来handle。例如try … except

下面直接通过duck typing相关的一个例子来展现两者的区别。

LBYL(Look Before You Leap)

class Duck:
    def quack(self):
        print('Quack, quack')
    def fly(self):
        print('Flap, Flap!')

class Person:
    def quack(self):
        print("I'm Quacking Like a Duck!")
    def fly(self):
        print("I'm Flapping my Arms!")

def quack_and_fly(thing):

    # LBYL (Non-Pythonic)
    if hasattr(thing, 'quack'):
    	if callable(thing.quack):
        	thing.quack()

	if hasattr(thing, 'fly'):
        if callable(thing.fly):
            thing.fly()
            
d = Duck()
print(type(dir(d)))

EAFP(Easier to Ask for Forgiveness than Permission)

class Duck:
    def quack(self):
        print('Quack, quack')
    def fly(self):
        print('Flap, Flap!')

class Person:
    def quack(self):
        print("I'm Quacking Like a Duck!")
    def fly(self):
        print("I'm Flapping my Arms!")

def quack_and_fly(thing):

    try:
        thing.quack()
        thing.fly()
        thing.bark()
    except AttributeError as e:
        print(e)

d = Duck()
print(type(dir(d)))

两类原则对比

此处摘自知乎:https://zhuanlan.zhihu.com/p/36167239
个人见解与补充使用黑体标记。

代码效率

EAFP 的异常处理往往也会影响一点性能,因为在发生异常的时候,程序会进行保留现场、回溯traceback等操作,但在异常发生频率比较低的情况下,性能相差的并不是很大。

LBYL 则会消耗更高的固定成本,因为无论成败与否,总是执行额外的检查。

相比之下,如果不引发异常,EAFP 更优一些。

个人认为:LBYL需要预先对所有可能的情况进行检查,如果可能性太多,会导致代码看起来非常丑,尤其是在工程化代码中肯定是会被diss的,且不易后续维护。而EAFP不应该仅仅使用exception来handle异常,在特定情况下应当直接raise error,例如数据对不上,维度不对,而不是等到最后出结果才发现异常,那样的话时间成本太高了。

代码易读性

当性能速度不用作考虑条件时,来看看代码的易读性。EAFP将业务逻辑代码,跟防御性代码隔离开,让开发者可以更专注于开发业务逻辑,不管数据变量是否合理,按照正常的逻辑思维执行下去,如果发生错误就在异常里面纠正。

LBYL则容易打乱开发者的思维,在做一件事之前,总是要先要判断能不能这样做,传入的数据是否合理,类型是否正确,需要增加很多判断内容,代码连贯性差,常见的一堆 if 判断,多数就是这种风格导致。

代码风险

在一个多线程的环境中,LBYL 面临着一个风险,即条件判断与紧接着的代码执行的竞争。如果代码涉及到原子操作,强烈推荐使用 EAFP 风格,比如某段程序的逻辑是对文件/数据库操作,使用 LBYL 风格:

if exists(file):
    do_something()
# or
if has(key):
    do_something()

就变成了2步操作,在多线程并发的时候,可能文件的状态或者key 的状态已经被其它现成改变了,而 EAFP 风格则可以确保原子性。

try
    read_file(file)
except :
    pass

并发编程的原子性: 一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。

两种风格存在的必要
  • 通过上面的对比,会感觉 EAFP要比 LBYL 哪方便都好一些。既然是这样,为什么还要讨论对比呢?
  • 之前在编写Java/C/C++的时候通常都是采用 LBYL风格,很少在一些不可控的情况才会使用 EAFP 风格。Python 的动态类型(duck typing)决定了 EAFP,而 Java等强类型(strong typing)决定了 LBYL。语言之间的设计哲学差异,Java 对类型要求非常严格,要求明确,类/方法等,它假定你应该知道,任何时候你正在使用的对象类型,以及它能做什么。相反,Python 的鸭子类型意味着你不必明确的知道对象的显示类型是什么,你只需要关心你在使用时候它能有相应的反馈。在这种宽松的限制下,唯一明确的态度就是认为代码会工作,准备面对结果。
  • 这无关语言的好坏,每一门语言都有自己的哲学与态度,正确的对待,理解。
结论
  • 如果有潜在不可控的问题,使用 EAFP
  • 如果预先检查成本很高,请使用 EAFP
  • 如果您希望操作在大多数时间成功,请使用 EAFP
  • 如果您预计操作失败的时间超过一半,请使用 LBYL
  • 如果速度不重要,使用您认为更易读的风格

协议(让Python这种动态类型语言实现多态的方式)

在面向对象编程中,协议是非正式的接口,是一组方法,但只是一种文档,语言不对施加特定的措施或者强制实现。虽然协议是非正式的,在Python中,应该把协议当成正式的接口。

Python中存在多种协议,用于实现鸭子类型(对象的类型无关紧要,只要实现了特定的协议(一组方法)即可)。需要成为相对应的鸭子类型,那就实现相关的协议,即相关的__method__。例如,实现序列协议__len__,以及__getitem__,这个类就表现得像序列。

协议是正式的,没有强制力,可以根据具体场景实现一个具体协议的一部分。例如,为了支持迭代,只需实现__getitem__,不需要实现__len__。

在Python文档中,如果看到“文件类对象“(表现得像文件的对象),通常说的就是协议,这个对象就是鸭子类型。这是一种简短的说法,意思是:“行为基本与文件一致,实现了部分文件接口,满足上下文相关需求的东西。” 举例,在前文解释鸭子类型的定义时,stringIO和GzipFile就是文件类对象。

关于具体的Python对象协议,此处暂不深入探讨

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值