Python的鸭子类型和多态
这个概念的名字来源于由James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为”鸭子”的对象,并调用它的”走”和”叫”方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的”走”和”叫”方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的”走”和”叫”方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名。
调用不同的子类将会产生不同的行为,而无须明确知道这个子类实际上是什么,这是多态的重要应用场景。而在python中,因为鸭子类型(duck typing)使得其多态不是那么酷。
鸭子类型是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由”当前方法和属性的集合”决定。
所有的类都实现了一种方法,则这些类可以归为一种类。
抽象基类(abc模块)
首先明确一点,抽象基类是无法实例化的。Python类的特性是根据实现的魔法函数决定的,而不是继承什么类,注意和静态语言的区分。
静态语言和动态语言的区别
静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。
静态类型语言的优点首先是在编译时就能发现类型不匹配的错误,编辑器可以帮助我们提前避免程序在运行期间有可能发生的一些错误。其次,如果在程序中明确地规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序执行速度。
静态类型语言的缺点首先是迫使程序员依照强契约来编写程序,为每个变量规定数据类型,归根结底只是辅助我们编写可靠性高程序的一种手段,而不是编写程序的目的,毕竟大部分人编写程序的目的是为了完成需求交付生产。其次,类型的声明也会增加更多的代码,在程序编写过程中,这些细节会让程序员的精力从思考业务逻辑上分散开来。
动态类型语言的优点是编写的代码数量更少,看起来也更加简洁,程序员可以把精力更多地放在业务逻辑上面。虽然不区分类型在某些情况下会让程序变得难以理解,但整体而言,代码量越少,越专注于逻辑表达,对阅读程序是越有帮助的。
动态类型语言的缺点是无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。
抽象基类定义(个人理解)
在基础的类当中我们设定好一些方法,所有的继承基类的类都要覆盖这些方法。
抽象基类的适用场景
场景一:判定某个类的类型
1 | class Company(object): |
如果我们不使用抽象基类,想判断类Company是否有长度属性__len__
我们只能使用hasattr
。
我们可以使用抽象基类判定是否是有长度的类型。
1 | from collections.abc import Sized |
上面的这个print
的输出是True,为什么呢?我们并没有继承Sized
。
我们可以看下Sized
的源码:
1 | class Sized(metaclass=ABCMeta): |
我们看到在源码中实现了一个__subclasshook__
用于检测是否包含__len__
方法,如果包含则返回True。
这里我们可以进一步理解Python中的鸭子类型,只要实现了对象的方法,就是某种类型。
场景二:我们需要强制某个子类必须实现某些方法
实现了一个web框架,集成cache(redis, cache, memorychache)
需要设计一个抽象基类, 指定子类必须实现某些方法
1 | import abc |
我们创建了一个抽象基类CacheBase
通过继承abc.ABCMeta
然后在抽象基类中指定子类必须实现的两个方法,get
和set
。当子类不实现这两个方法的时候回直接抛出异常。
isinstance和type的区别
我们首先定义两个类
1 | class A: |
我们使用ininstance
判断类的从属
1 | print(isinstance(b, B)) |
然后我们使用type
判断
1 | print(type(b) is B) |
第二个判断为False是因为type(b)
的指向对象B,虽然B继承了A,但是A和B是两个不同的对象。
结论:我们尽量用ininstance
判断类的类型。
类变量和实例变量
首先明确类变量和实例变量是两个独立的变量。
1 | class A: |
类属性和实例属性的查找顺序
优先明确什么是类属性:定义在类内部的变量或方法都是属性。
我们先看个简单的顺序查找
1 | class A: |
当实例对象查找属性name
的时候是由下往上查找的,首先查找实例内部是否存在,然后再查找类属性。
我们看下多继承情况下的MRO算法:
上图是深度优先算法。
但是当时这种菱形继承的时候,就会出现问题。因为B和C都继承了D。如果C重写了D中的方法 是无法被覆盖的(执行到C)
广度优先解决深度优先菱形问题
我们看下上图的广度优先算法 也是有一定弊端的
但是当继承关系如上图所示s的时候,因为优先继承的是B,当在B查找不到,而D中存在对应属性的时候会被C覆盖掉。
Python3之后的查找算法更改为C3算法。
我们看下上图在Python3中的中的继承关系。
1 | #新式类 |
查找顺序是A-B-D-C-E。上述存在的问题就不会再存在了。
静态方法,实例方法,类方法
我们通过代码查看区别
1 | class Date: |
数据封装和私有属性
1 | from datetime import date |
Python的数据封装是指在属性前面增加__
完成。
Python将私有属性伪装成_User__birthday
(类名拼接)我们直接调用_User__birthday
也是可以调用私有属性的。对于继承,同样适用。
Python对象的自省机制
自省就是通过一定的机制查询到对象的内部结构。
1 | class Person: |
这里__dict__
获得的是对象的属性,而实例user
的属性是{'school_name': '浙大'}
并没有name
,为何能打印输出呢?
这是因为name
是类Person
的属性,而Student
继承它,根据属性查找顺序(根据继承关系,从下往上查找),我们就能获得name
的值。
我们查看一下Person
类的属性
1 | print(Person.__dict__) |
我们看到里面的属性name
。
我们可以通过__dict__
动态的给对象赋予属性。
1 | user.__dict__["school_addr"] = "北京市" |
我们也可以使用dir
获得对象的属性有哪些,但是只有属性,没有属性值。
1 | a = [1,2] |
Python中的super调用
我们看下代码
1 | class A: |
这里我们带着两个疑问看下面的代码
- 既然我们重写B的构造函数, 为什么还要去调用super?
- super到底执行顺序是什么样的?
问题一:当我们想要重用父类的一些属性的时候
下面我们自己定义一个线程类,通过继承
1 | from threading import Thread |
这时我们可以使用super
去调用父类的初始化函数,是因为在Thread
类的初始化方法里已经定义了name
。
1 | # Thread的初始化方法 |
问题二:super并不是简单地调用父类
1 | class A: |
从上面的输出我们可以看出super的调用顺序是指MRO算法的下一个类的构造函数。
django rest framework中对多继承使用的经验
在Python中并不支持多继承。推荐使用mixin混合模式。
mixin模式特点
- Mixin类功能单一
- 不和基类关联,可以和任意基类组合, 基类可以不和mixin关联就能初始化成功
- 在mixin中不要使用super这种用法
Python中的with语句
我们看看下面的代码输出
1 | # try except finally |
为什么return的是4而不是2呢?
这是因为在 try except finally语句中,存在return的语句会先将值压到栈中,最后只取栈顶数据。2在4之前压入栈,因此返回的数据是4。
下面我们看下上下文管理器(with语句),可以大大简化 try except finally 语句。
1 | # 上下文管理器协议 |
我们定义了一个类,实现了两个魔法函数__enter__
, __exit__
,就实现了一个上下文管理器。
使用contextlib简化上下文管理器
我们先看下代码
1 | import contextlib |
装饰器contextlib.contextmanager
修饰的是一个生成器函数。然后将函数变成上下问管理器(利用了生成器的特性)。