python面向对象
面向对象的基本概念
过程与函数
- 过程是早期的一个编程概念,类似于函数,只能执行,但是没有返回值
- 函数不仅能执行,还可以返回结果
面向过程开发
- 把完成某一个需求的所有步骤从头到尾逐步实现
- 根据开发需求,将某些功能独立的代码封装成一个又一个函数
最后完成的代码就是顺序地调用不同的函数 - 特点
⭕ 注重步骤与过程,不注重职责分工
⭕ 如果需求复杂,则代码会变得更复杂
⭕ 没有固定的套路,开发难度很大
面向对象开发
- 相比较函数,面向对象是更大的封装,根据职责在一个对象中封装多个方法
- 在完成某一个需求前,先要确定职责,即要做的事请(也叫方法)
- 根据职责确定不同对象,在对象内部封装多个不同方法
- 最后完成的代码就是顺序地让不同对象调用不同方法
- 特点
⭕ 注重对象和职责,不同对象承担不同职责
⭕ 更加适合应对复杂的需求变化,专门应对复杂项目开发提供固定的套路
类
- 类是一群具有相同特征或者行为的事物的一个统称
- 类是抽象的,不能直接使用
- 类的职责就是负责创建对象
对象
- 对象是由类创建出来的一个具体实例
- 类是模板,对象是根据类创建出来的
- 在程序开发中,先有类,再有对象
- 不同对象之间的属性可能会各不相同
- 对象的属性取决于类
- 通常把创建出来的对象叫做类的实例
- 创建对象的动作叫做实例化
- 对象的属性叫做实例属性
- 对象调用的方法叫做实例方法
- 使用类名()创建对象的动作有两步:
⭕ 在内存中为对象分配空间
⭕ 调用初始化方法__init__ 为对象初始化 - 每一个对象都有自己的独立的内存空间,保存各自不同的属性
- 多个对象的方法,在内存中只有一份,在调用方法时,需要把对象的引用传递到方法内部
- 类是一个特殊的对象
- 在程序运行时,类同样会被加载到内存
- 类对象在内存中只有一份(相当于一个模板),一个类可以创建出很多个对象实例
- 除了封装实例的属性和方法外,类对象还可以拥有自己的属性和方法
- 类属性通常用于记录与这个类相关的特征,而不会用于记录具体对象的特征
类的三要素:类名(满足大驼峰命名法)、属性、方法
- dir内置函数
- 使用内置函数dir传入标识符或者数据,可以查看对象内的所有属性和方法
- __方法名__是python提供的内置方法或属性
self参数的使用
- 由哪一个对象调用的方法,方法内的self就是哪一个对象的引用
- 在类封装的方法内部,self就表示当前调用方法的对象自己
- 调用方法时不需要传递self参数
- 在方法内部可以通过self. 访问对象属性
- 也可以通过self. 调用其他的对象方法
- 在定义方法时,第一个参数必须是self,否则在方法内部无法通过 self. 访问到对象的属性
__init__方法
- 初始化方法是专门用来定义一个类具有哪些属性的方法
- __ init__方法是对象的内置方法
- 使用类名()创建对象的时候,会自动调用初始化方法__init__
- 在初始化方法中定义的属性,会使所有实例化的对象都具有该属性
__del __方法
- 当一个对象从内存中被销毁前,会自动调用__del__方法
- 当使用类名()创建一个对象时,该对象生命周期开始,如果一个对象的__del__方法被调用,则该对象的生命周期结束
- 如果希望在对象被销毁前再做一些事情,可以考虑使用__del__方法
__str __方法
- 如果希望使用print方法输出对象变量时能够打印自定义内容,可以使用__str__内置方法
- __str __方法返回值必须是字符串类型
定义私有属性和私有方法
- 在python中,私有属性和私有方法的定义就是在属性名或者方法名前增加两个下划线__
- 私有属性和私有方法只能在对象内部被使用,而不会被外部访问
伪私有属性和方法
- 在python中,没有真正意义上的私有属性和私有方法,只是对名称做了一些特殊的处理,使得外界无法访问到
- 如果想在外界调用私有属性和私有方法,可以使用如下格式进行调用
_类名__私有属性(或者私有方法)
私有属性和私有方法扩展
- 子类对象不能在自己的方法内部,直接访问父类的私有属性或者私有方法
- 可以使用 实例对象.__ dict__ 内置方法查看实例对象(或者 类对象.__ dict__ 查看类对象)下面的所有属性
- 子类对象可以通过父类的公共方法(例如访问器get和修改器set)间接访问到私有属性或者私有方法
类方法和静态方法
- 类方法:针对类对象定义的方法
- 在类方法的内部可以直接访问类属性或者调用其他的类方法
- 类方法的语法如下
- 类方法需要用修饰(装饰)器 @classmethod来标识,告诉解释器这是一个类方法
- 类方法的第一个参数通常是cls,其语法与实例方法中的self类似
- 子类可以通过__class__访问类对象的类方法
- 静态方法:如果需要在类中封装一个方法,这个方法既不需要访问实例属性或者调用实例方法,也不需要访问类属性或者调用类方法,则这个方法可以封装成静态方法
- 静态方法语法如下
- 静态方法需要用修饰()装饰器 @staticmethod来标识,告诉解释器这是一个静态方法
- 静态方法的调用类似于类方法的调用
- 静态方法的调用不需要创建对象
- 定义方法时实例方法、类方法、静态方法的确定
⭕ 封装的方法内部需要访问到对象的实例属性,则方法定义为实例方法
⭕ 封装的方法内部只需要访问到类的类属性,则方法定义为类方法
⭕ 封装的方法内部不需要访问实例属性和类属性,则方法定义为静态方法
⭕ 封装的方法内部即需要访问到对象的实例属性,又需要访问到类的类属性,则方法定义为实例方法(因为类只有一个,在实例方法内部可以使用 类名. 访问类属性)
属性的获取机制
- 在python中属性的获取存在一个向上查找的机制,如下图所示(假设定义了一个Tool工具类,并且实例化了一个tool1对象叫水桶)
- 因此,要访问类属性有两种方式
⭕ 类名.类属性
⭕ 对象.类属性 - 注意:如果使用对象.类属性 = 值赋值语句,只会给对象内部添加一个新的实例属性,而不会影响到类属性的值
继承
面向对象的三大特性
- 封装(定义类的准则):根据职责将属性和方法封装到一个抽象的类中
- 继承(设计类的技巧):实现代码的重用
- 多态(调用方法的技巧):不同的对象调用相同的方法,产生不同的执行结果,增加代码的灵活度
单继承
- **继承(派生)**的概念:子类(派生类)拥有父类(基类)的所有属性和方法
- 继承的语法:class 类名(父类名)
- 子类继承自父类,可以直接获得父类中已经封装好的方法,不需要再次开发
- 子类中根据职责,封装子类特有的属性和方法
- 继承的特性:传递性(即子类拥有父类以及父类的父类中封装的所有属性和方法)
- 一个类不能继承没有继承关系的其他类
- 父类方法的重写
⭕ 重写(覆盖)父类的方法:在子类中定义一个与父类同名的方法并且实现,重写之后,在运行时,只会调用子类中重写的方法,而不会调用父类封装的方法
⭕ 对父类的方法进行扩展(即子类的方法实现中包含父类的方法实现):使用扩展的方式即可,即在需要的位置使用super().父类方法来调用父类方法的执行,代码的其他位置针对子类的需要,编写子类特有的代码实现 - python2.x中对父类方法重写还有另外一种格式:父类名.父类方法(self) ,但是不推荐使用
多继承
- 多继承的概念:在子类中可以拥有多个父类,并且具有所有父类的属性和方法
- 多继承的语法:class 子类名(父类名1,父类名2…)
- 多继承可以让子类对象同时具有多个父类的属性和方法
- 多继承使用父类方法有三种:用
父类名调用父类方法()(使用父类名需要加上self)、用super()调用父类方法(使用super不需要加self)、使用super(类名)调用父类方法
⭕ 使用父类名调用父类方法可能会使得被多个类继承的类调用了很多次
⭕ 在类中使用super()会遵循该类的内置属性__mro__,__mro__返回的是一个元组,元组中保存super()调用类的先后顺序,顺序通过python3解释器中默认的C3算法(C3算法也叫C3超类线性化算法,该算法确定每个类只会调用一次,本质是一个排序算法
)进行得出,因此调用super()调用的不一定是父类
C3算法:算法执行过程:
遍历merge中各个列表,当列表的第一个数据,在其他列表中也是同一个数据 或者 其他列表中没有这个数据,则把这个数据提到merge前面的列表中append进去。而在merge里面的这些列表,都把该数据删除,直到merge中所有序列都为空,则merge前面的列表就是它的方法解释顺序。
举例:
O=object
class f(O):pass
class e(O):pass
class d(O):pass
class c(d,f):pass
class b(e,d):pass
class a(b,c):pass
print(a.mro())
L[f]=fo L[e]=eo L[d]=do
L[c]=c+merge(do,fo,df)
L[c]=cdfo
L[b]=b+merge(eo,do,ed)
L[b]=bedo
L[a]=a+merge(do,dfo)
L[a]=abecdfo
如果最后merge中还存在不为空的列表,则说明该类的继承关系存在问题!
C3算法与拓扑算法的区别:
拓扑算法未考虑基类出现的先后顺序。
在特定的继承关系下,通过拓扑算法分析出的解释顺序与C3算法计算出的解释顺序不一致。
⭕ 调用super()从当前类名开始到__mro__内置方法中进行查找,调用super(类名)直接从类名到__mro__内置方法中进行查找
5. 单继承的用super()和用父类名调用父类的对象结果是相同的
6. 单继承与多继承中使用super()和使用父类名字调用父类函数总结
:
⭕ super().__init__相对于类名.init,在单继承上用法基本无差
⭕ 但在多继承上有区别,super方法能保证每个父类的方法只会执行一次,而使用类名的方法会导致方法被执行多次
⭕ 多继承时,使用super方法,对父类的传参数,应该是由于python中super的算法导致的原因,必须把参数全部传递,否则会报错
⭕ 单继承时,使用super方法,则不能全部传递,只能传父类方法所需的参数,否则会报错
⭕ 多继承时,相对于使用类名.__init__方法,要把每个父类全部写一遍, 而使用super方法,只需写一句话便执行了全部父类的方法,这也是为何多继承需要全部传参的一个原因
7. 多继承的使用注意事项
⭕ 如果不同的父类中存在同名的方法容易产生混淆的情况,应该尽量避免使用多继承
⭕ 子类继承多个父类时存在一定的顺序
8. python中的MRO(方法搜索顺序)
⭕ python中针对类提供了一个内置属性__mro__可以查看方法搜索顺序
⭕ MRO是method resolution order,主要用于在多继承时判断方法、属性的调用路径
⭕ 举例 :print(C.__mro __) (其中C继承了A和B两个父类)
输出结果为一个元组如下:
⭕ 在搜索方法时,是按照__mro__的输出结果从左至右的顺序查找的
⭕ 如果在当前类中找到方法,就直接执行,不再搜索
⭕ 如果没有找到,就查找下一个类中是否有对应的方法,如果找到,就直接执行,不再搜索
⭕ 如果找到最后一个类仍然没有找到方法,程序报错
9. python中的新式类与旧式(经典)类
⭕ object是python中为所有对象提供的基类,提供有一些内置的属性和方法,可以使用dir函数查看
⭕ 新式类:以object为基类的类
⭕ 旧式类:不以object为基类的类
⭕ 在python3.x中定义类时,如果没有指定基类,就会默认使用object作为该类的基类,因此python3.x中定义的类都是新式类
⭕ 而在python2.x中定义类时,如果没有指定基类,则不会以object作为基类
⭕ 新式类和旧式类在多继承时会影响到方法的搜索顺序
⭕ 因此在定义类时,如果没有父类,建议写上继承自object
多态
- 多态:不同的子类对象调用相同的父类方法,产生不同的执行结果
- 多态可以增加代码的灵活度
- 多态以继承和重写父类方法这两个条件为前提
- 多态不会影响到类的内部设计
设计模式
- 设计模式:设计模式是前人工作的总结和提炼,通常设计模式都是针对某一特定问题的成熟的解决方案
- 使用设计模式是为了可重用代码,让代码更容易被他人理解、保证代码的可靠性
__new__方法
- 使用类名()创建对象时,python解释器首先会调用__new__方法为对象分配空间
- __new__方法是一个由object基类提供的内置静态方法,主要作用有两个
⭕ 在内存中为对象分配空间
⭕ 返回对象的引用 - python的解释器获得对象的引用后,将引用作为第一个参数,传递给__init__ 方法
- 重写__new__方法一定要返回 return super().new(cls)
- 否则python的解释器得不到分配了空间的对象引用,就不会调用对象的初始化方式方法
- 注意:__new__是一个静态方法,在调用时需要主动传递cls参数
单例设计模式
- 单例设计模式目的:让类创建的对象,在系统中只有唯一的一个实例
- 每一次执行类名()返回的对象,内存地址是相同的
- 应用场景:音乐播放对象、回收站对象、打印机对象等等
- 单例模式的建立步骤如下
⭕ 定义一个类属性,初始值为None,用于记录单例对象的引用
⭕ 重写__new__方法
⭕ 如果类属性 is None,调用父类方法分配空间,并在类属性中记录结果
⭕ 返回类属性中记录的对象引用 - 只执行一次初始化工作
⭕ 在每次使用类名()创建对象时,python解释器都会自动调用__init__初始化方法
⭕ 如果只想让初始化动作只被执行一次,可以按照以下步骤
Ⅰ. 定义一个类属性 init_flag标记是否执行过初始化动作,初始值为False
Ⅱ. 在__init__方法中,判断init_flag,如果为False就执行初始化动作
Ⅲ. 然后将init_flag设置为True
Ⅳ. 这样,再次自动调用__init__方法时,初始化动作(注意是动作,python解释器自动调用__init__方法是无法被限制的)就不会被再次执行了
异常
- 异常的概念:程序在运行时,如果python解释器遇到一个错误,会停止程序的执行,并且提示一些错误的信息,这就是异常
- 程序停止执行并且提示错误信息这个动作,通常称之为抛出(raise)异常
- 程序开发流程的示意图如下
- 程序开发时,很难将所有的特殊情况都处理到,通过异常捕获可以针对突发事件做集中的处理,从而保证程序的稳定性和健壮性
捕获异常
- 在程序开发中,如果对某些代码的执行不能确定是否正确,可以增加try来捕获异常
- 捕获异常的最简单语法格式如下
- 程序执行时,可以会遇到不同类型的异常,并且需要针对不同类型的异常做成不同的响应,这就需要捕获错误类型
- 捕获错误类型语法如下
- 当python解释器抛出异常时,最后一行错误信息的第一个单词,就是错误类型
捕获未知错误
- 在开发时,要预判到所有可能出现的错误还是有一定难度的
- 如果希望出现无论出现任何错误,都不会因为python解释器抛出异常而终止,可以再增加一个异常处理:except Exception as 变量名
- 异常捕获的完整语法如下
异常的传递
- 当函数或者方法执行出现异常时,会将异常传递给函数或者方法的调用一方
- 如果传递到主程序,仍然没有异常处理,程序才会被终止
- 可以在主函数中增加异常捕获
- 而在主函数中调用的其他函数,只要出现异常,都会传递到主函数的异常捕获中,这样就不需要再代码中增加大量的异常捕获
主动抛出raise异常
- 除了代码执行出错python解释器会抛出异常之外,还可以根据应用程序特有的业务需求主动抛出异常
- python中提供了一个Exception异常类
- 如果满足特定业务需求时,希望抛出异常,可以创建一个Exception异常类的对象,然后使用raise抛出异常对象
模块和包
- 模块:每一个以扩展名py结尾的python源代码文件都是一个模块
- 模块的两种导入方式:import导入和 from 模块名 import
- import导入是一次性把模块中的所有工具全部导入
- import导入模块可以使用as在模块名后面加上模块的别名
- from 模块名 import 导入:从某一个模块中导入部分工具
- from 模块名 import 导入之后不需要通过 模块名. 的方式使用模块中的工具,即可以直接使用模块提供的工具(全局变量、函数、类等)
- 注意:如果两个模块存在同名的函数,那么后导的模块的函数会覆盖掉先导入模块的函数
- 如果两个模块存在同名函数,可以用as给其中一个工具起一个别名
- 注意:在导入文件时,文件中所有没有任何缩进的代码都会被执行一遍
- __name__内置属性:测试模块的代码只在测试情况下被运行,而在被导入时不会被执行
- 如果是当前执行的程序,__ name__输出的是__main__
- 如果是被其他文件导入的,__name__输出的是模块名
模块的搜索顺序
- python解释器在导入模块时,会搜索当前目录指定模块名的文件,如果有就直接导入,如果没有,再搜索系统目录
- python中每一个模块都有一个内置属性__file__可以查看模块的完整路径
包
- 包:包是一个包含多个模块的特殊目录
- 目录下有一个特殊的文件 __ init__.py
- 使用import 包名可以一次性导入包中所有的模块
eval函数
- eval()函数十分强大:将字符串当成有效的表达式来求值并返回计算结果
- eval()函数会把传入字符串两端的引号去掉,然后引号内的内容当作一行python的代码
- 注意:开发时不要使用eval()函数直接转换input的结果