多态
前言
面向对象的三大特征分别是:封装,继承,多态,如下所示:
封装是根据需求将属性和方法封装到一个抽象的类中
继承则能够实现代码的重用,相同的代码不需要重复编写;
多态:不同的子类对象调用相同的父类方法,产生不同的执行结果,多态的思路:
- 多态可以增加代码的灵活度
- 以继承和重写父类方法为前提
- 是调用方法的技巧,不会影响到类的内部设计
示意图如下所示:
在这个示意图中,假设我们定义了一个人类,然后从人类中派生出了程序员类与设计师类,而程序员类的工作产出了代码,设计师的工作产生了设计图,这两个类都有一个共同的父类,即人类,但他们的产出则不同。
在设计代码时,通常是先封装,有了封装后,等到代码比较复杂时,我们就会用到继承,产生各种子类,有些子类会继承自相同的父类,这就是多态,代码的灵活度就会增强。
多态(Polymorphism)
书中原文说的多态是指:术语多态(polymorphism)源自希腊语,意思是“有多种形态”。这大致意味着即便你不知道变量指向的是哪种对象,也能够对其执行操作,且操作的行为将随对象所属的类型(类)而异。
书中列举了几个案例,例如当你收到一个对象时,却根本不知道它是如何实现的,它就有可能是众多“多态”中的任何一种,例如当设计一个购物系统中,有一项功能是获取某个物品的价格,如下所示:
|
|
像这样与对象属性相关联的函数称为方法,在常规的Python对象,例如字符串、列表或字典中也会见到这样的方法,例如:
|
|
就像上面的案例,如果有一个变量x,你需要知道它是字符串,还是列表,就能调用方法count,你只需要向这个方法提供一个字符作为参数,它就能正常运行,这就是多态的表现之一。下面再来做一个实验,标准库模块random包含一个名为choice的函数,它会从序列中随机选择一个元素,如下所示:
|
|
x可能包含字符串`Hello, world!‘,也可能包含列表[1,2,’e’,’e’,4],具体是哪一个,我们并不需要关心,我们只关心x包含多个e,而不管x是字符串还是列表,此时我们再调用count方法,如下所示:
|
|
从结果来看,x包括的应该是字符串(其实前面也显示了这个结果),但关键在于你无需执行相关的检查,只要x有一个名为count的方法,它将单个字符作为参数并返回一个整数就行。
多态形式的多样性
每当无需知道对象是什么样时,就能对其进行操作时,就是多态在起作用。这不仅仅适用于方法,内置运算符和函数都大量使用了多态,可以看下面的案例:
|
|
上面的案例说明,+
运算符即可以用于整数的加法,还可以用于字符串的连接,这就是多态的体现。
再看一个案例,如果要编写一个函数,通过打印一条消息来指出对象的长度,如下所示:
|
|
运行结果为:
|
|
可以看出来,无论对象是列表,还是字符串,此函数都能运行。当使用多有态的函数和运算符时,多态都将发挥作用,事实上,要破坏多态,唯一的办法就是使用诸如type,issubclass等函数显式地执行类型检查,但你应尽可能避免以这种方式破坏多态。
多态的案例
现在我们来看一个案例需求:
- 我们需要定义3个类,分别是人类(Person),狗类(Dog),哮天犬类(XiaoTianQuan)类;
- 狗类(Dog)中封装一个玩耍的方法(game);
- 定义一个哮天犬类(XiaoTianQuan)类,这个类继承自(Dog),但是这个类中的game方法需要进行修改,例如普通玩耍变成飞到天上玩耍;
- 定义一个人类(Person),在这个类中,让人(Person)与狗类(Dog)玩耍(game),示意图如下所示:
代码如下所示:
|
|
结果运行如下所示:
|
|
从结果可以看出来,不同的狗,(Dog)与(XiaoTianQuan)调用相的方法时,产生的结果不一样。
实例
面向对象开发步骤如下:
- 使用面向对象开发,第1步是设计类;
- 使用
类名()
创建对象,创建对象的动作有两步:①在内存中为对象分配空间;②调用初始化方法__init__
为对象初始化; - 对象创建后,内存中就有一个对象的实实在在的存在,这个存在就是实例。
因此,通常也会有以下这种叫法:
- 创建出来的对象叫做类的实例;
- 创建对象的动作叫做实例化;
- 对象的属性叫做实例属性;
- 对象调用的方法叫做实例方法。
在程序执行时:
- 对象各自拥有自己的实例属性;
- 调用对象方法,可以通过
self
来访问自己的属性,以及调用自己的方法。
结论
- 每一个对象都有自己独立的内存空间,保存各自不同的属性;
- 多个对象的方法,在内存中只有一份,在调用方法时,需要把对象的引用传递到方法内部。
类是一个特殊的对象
在学习Python时,我们常常听到这样的话:
Python
中一切皆无明:
class AAA:
定义的类属于类对象obj1 = AAA()
属于实例对象
- 在程序运行时,类同样会被加载到内存中;
- 在
Python
中,类是一个特殊的对象——类对象; - 在程序运行时,类对象在内存中只有一份,使用一个类可以创建出很多个对象实例;
- 除了封装实例的属性和方法外,类对象还可以拥有自己的属性和方法:即①类属性;②类方法。
- 通过类名.的方式哦可以访问类的属性或者调用类的方法,如下所示:
补充知识:什么是对象
在面向对象的编程中,经常听到的一句话就是“一切皆对象”这句话到底是什么意思,可以看知乎的一个帖子,从Python的源码角度解释的:《关于python中“赋值就是建立一个对象的引用”,大家怎么看?》
类属性和实例属性
概念和使用
- 类属性主浊给类对象中定义的属性
- 通常用来记录与这个类相关的特征
- 类属性不会以用于记录具体对象的特征。
示例需求
我们先来看一个案例:
- 定义一个工具类
- 每件工具都有自己的
name
- 需求——知道使用这个类,创建了多少个工具对象?
如下所示:
|
|
运行结果如下所示:
|
|
从代码中我们可以看出来,我们先定义了一个Tool
类,并且定义了相应的类属性,用来记录与这个类相关的特征。
属性的获取机制
属性的获取机制指的是,我们要编写代码时,在一个变量的后面接一点,再接这个变量的属性,Python的解释器是如何找到这个值的。
Python中属性的获取存在一个向上查找机制。这个机制的具体表现就是,先在对象的属性中查找类属性,如果没有找到类属性,就向上,向类中寻找这个类属性,如下所示:
因此,要访问类属性有两种方式:①类名.类属性;②对象.类属性(不推荐)。
需要注意的是,如何使用对象.类属性 = 值
这样的赋值语句,只会给对象添加一个属性,而不会影响到类属性的值(从前面的笔记中可以了解到这些内容)。
前面我们看到,当我们输出print("工具/对象总数 %d" % tool3.count)
这个结果时,结果为3
,因为通过对象可以访问类的创建数目,但是,如果我们使用了对象.类属性 = 值
这样的赋值语句时,就会出问题,如下所示:
|
|
结果如下所示:
|
|
从结果可以看出来,tools.count
它此时就代表的是不是类属性的值(类的值是Tool.count
),而是又给它赋的值(因为解释器会先查找对象内部的count,如果没有,它再向上,在类中寻找count),因此在访问类的属性时,并不采用这种方式(也就是对过对象.属性
这样的方式)。
类方法和静态方法
注:需要补充类方法与实例方法的区别,以及应用范围,什么情况下使用。
类方法
类属性就是针对类对象定义的属性
- 使用赋值语句在
class
关键字下方可以定义类属性; - 类属性用于记录与这个类相关的特征
- 使用赋值语句在
类方法就是针对类对象定义的方法
- 在类方法内部可以直接访问类属性或者调用其他的类方法
类方法语法
类方法的语法与实例方法的语法非常类似,如下所示:
|
|
关于类方法需要注意以下几点:
- 类方法需要用修饰器(
@classmethod
)来标记,告诉解释器这是一个类方法; - 类方法的第一个参数应该是
cls
:- 由哪一个类调用的方法,方法内的
cls
就是哪一个类的引用; - 这个参数和实例方法的第一个参数是
self
类似; - 提示使用其他名称也可以,不过习惯使用
cls
- 由哪一个类调用的方法,方法内的
- 通过
类名.
调用类方法
,调用方法时,不需要传递cls
参数; - 在方法内部:
- 可以通过
cls.
访问类的属性; - 也可以通过
cls.
调用其他的类方法。
- 可以通过
示意需求
现在我们看一下需求:
- 定义一个工具类;
- 每件工具都有自己的
name
; - 需求——在
类
中封装一个show_tool_count
的类方法,输出使用当前这个类创建的对象个数,那么需求的大致要求就如下所示:
|
|
完整的代码如下所示:
|
|
运行结果如下所示:
|
|
从上面的案例我们就知道,在Tool
这个类内部,我们定义了一个类方法,也就是@classmethod
这一部分的内容。
静态方法
如果我们在开发过程中,霜肆在类中封装一个方法,这个方法有以下特性:
- 不需要访问实例属性或者调用实例方法;
- 不需要访问类属性或者调用类方法。
这个时候,我们就可以把这个方法封装成一个静态方法,静态方法的语法格式如下所示:
|
|
- 静态方法需要用修饰器
@staticmethod
来标记,告诉解释器这是一个静态方法; - 通过
类名.
来调用静态方法。
下面来看一个简单的应用:
|
|
结果运行如下所示:
|
|
在这个案例中,我们并没有创建实例对象,就调用了类中的方法。这里再说一下什么时候需要创建静态方法,那就是既不访问实例属性,也不访问类属性的情况下,我们就可以定义一个静态方法。
案例分析
在这个案例中,我们先看一下需求:
- 设计一个
Game
类; - 属性:
- 定义一个
类属性
,即top_score
记录游戏的历史最高分; - 定义一个
实例属性
,即player_name
,记录当前游戏的玩家姓名。
- 定义一个
- 方法:
- 静态方法(
show_help
)显示游戏帮助信息; - 类方法(
show_top_score
)显示历史最高分,它跟所有的类有关; - 实例方法(
start_game
)开始当前玩家的游戏。
- 静态方法(
- 主程序步骤:
- 查看帮助信息;
- 查看历史最高分;
- 创建游戏对象,开始游戏。
那么这个游戏的大致框架如下所示:
|
|
具体代码如下所示:
|
|
结果运行如下所示:
|
|
案例总结
- 实例方法——方法内部需要访问实例属性,实例方法内部可以使用
类名.
来访问属性。 - 类方法——方法内部只需要访问类属性;
- 静态方法——谅地内部,不需要访问实例属性和类属性。
单例
单例设计模式
什么是设计模式?
直接引用菜鸟教程里面的话:
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
使用设计模型是为了可重用代码、让仍茇以更容易被他人理解、保证代码的可靠性。
- 目的——让类创建的对象,在系统中只有唯一的一个实例;
- 每一次执行
类名()
返回的对象,内存地址是相同的(内存地址相同,说明每次由这个类创建的这个对象只有一个)。
单例设计模式的应用场景
- 音乐播放对象(一次只播放一首歌曲)
- 回收站对象(一个电脑通常只有一个回收站)
- 打印机对象(打印一些文件时,通常是在同一台打印机进行打印)
- ……
__new__
方法
在Python中,当我们使用类名()
创建对象时,Python解释器首先会调用__new__
方法为对象分配空间。这个__new__
方法是一个由object基类提供的内置的静态方法,它的作用主要有两个:
- 1) 在内存溃为对象分配空间;
- 2)返回对象的引用。
当Python的解释器获得对象的引用后,将引用作为第一个参数,传递给__init__
方法。重写__new__
方法的代码非常固定,只需要注意以下几点:
重写
__new__
方法一定要return super().__new__(cls)
否则Python解释器得不到分配了空间的对象引用,就不会调用对象的初始化方法;
注意:
__new__
是一个静态方法,在调用时需要主动传递(cls)参数。
以创建一个MusicPlayer
类为例说明一下:
现在看一下代码,如下所示:
|
|
运行结果如下所示:
|
|
现在输出了对象中的内容(播放器初始化
),并且还输出了对象的内存地址。现在我们重写__new__
方法,如下所示:
|
|
结果运行如下所示:
|
|
此时的结果中出现了None
,这说明初始化方法并没有被调用(也就是没有出现播放器初始化
字样)。
现在回顾一下前面的内容,也就是下面这一段文字:
重写
__new__
方法一定要return super().__new__(cls)
再对照代码,也就是下面的这些内容:
def __new__(cls, *args, **kwargs): # 1. 创建对象时, new方法会被自动调用 print("创建对象, 分配空间")
因此我们在重写__new__
方法时,并没有写return super().__new__(cls)
,因此Python解释器就得不到分配了空间的对象引用,因此就不会调用对象的初始化方法(也就是输出播放器初始化
字样),只能输出None
,现在我们输入下面的代码:
|
|
完整的代码如下所示:
|
|
上述代码运行结果如下所示:
|
|
上面的案例就说明了__new__
方法的说用几。
Python中的单例
单例——让类创建的对象,在系统中只有唯一的一个实例,它的设计流程如下所示:
- 定义一个类属性,初始值是
None
,用于记录单例对象的引用 - 重写
__new__
方法; - 如果类属性
is None
,调用父类方法分配空间,并在类属性中记录结果; - 返回类属性中记录的对象引用,整个流程如下所示:
现在我们先来验证一个Python中单例是否是这个类创建出来的唯一实例,具体方法就是,我们创建几个对象,并返回对象的一内存地址,看它们是否一样,先来看一段简单的代码,如下所示:
|
|
结果运行如下所示:
|
|
从结果中我们可以发现,player1
和player2
这两个对象的内存地址并不相同,说明这两个对象是完全不同的对象。而单例设计模型则是,无论调用多少次对象创立的方法,得到的对象引用是相同的,也就是说控制台输出的内存地址都是相同的。
现在我们就来说明一下单例设计是如何实现的,流程就是前面的那些说明与示意图。
单例案例分析
在这个案例中,我们会看一下,无论我们创建了多少个实例,它们的内存地址都是一样的,也就是说由这个类创建出来的对象只有一个,如下所示:
|
|
运行结果如下所示:
|
|
从结果中我们可以看出来,player1
和player2
是一个对象,它们的内存地址是一样的。
只执行一次初始化工作
前面的暗到,当我们每次使用类名()
创建对象时,Python的解释器都会自动调用两个方法:①__new__
用来分配空间;②__init__
对象初始化。
在前面一部分我们对__new__
方法改造之后,每次都会得到第一次被创建对象的引用,但是,初始化方法还会被再次调用。
如果我们只想让初始化动作只被执行一次,那么就需要以下解决方法:
- 定义一个类属性
init_flag
标记是否执行过初始化动作,初始值为False
; - 在
__init__
方法中,判断init_flag
,如果为False
,就执行初始化动作; - 然后将
init_flag
设置为True
; - 这样,再次自动调用
__init__
方法时,初始化动作就不会补再次执行了。
我们来看一个案例,我们在前面代码的基础上,加上下面的这段代码,如下所示:
|
|
完整代码如下所示:
|
|
运行结果如下所示:
|
|
从结果中我们可以知道,我们用类创建了两个对象,因此初始化方法就被调用了2次。但是,我们在开发的过程中,有可能遇到这样的需求,也就是说,我们只想让初始化方法执行一次,解决方法前面已经提到,如下所示:
- 定义一个类属性
init_flag
标记是否执行过初始化动作,初始值为False
;- 在
__init__
方法中,判断init_flag
,如果为False
,就执行初始化动作;- 然后将
init_flag
设置为True
;- 这样,再次自动调用
__init__
方法时,初始化动作就不会补再次执行了。
现在我们把下面的代码加到原代码中:
|
|
完整代码如下所示:
|
|
运行结果如下所示:
|
|
将代码更改后,从运行结果中我们就可以发现,初始化动作就只被执行了1次。