22. 面向对象 - 高阶
Hi,大家好。我是茶桁。
之前的课程里面,我们简单的接触了面向对象编程,也和大家讲解了其思想,优缺点。相信上节课程结束之后,大家对面向对象都有了一定的理解。
那么我们这节课,就进入面向对象的一些高阶部分,让我们继续来学习一些魔术方法以及 Python 的内置成员,然后再来学习一下描述符与设计模式。
- 内置成员
- 魔术方法
- 描述符
- 设计模式
好,正课走起。让我们开始。
内置成员
当我们创建一个类之后,即便我们还什么都没做,这个类里面就已经有内容了,我们来看一下:
1 |
|
上节课我们学过了__dict__
,
这个是获取类或者对象的成员的方法。打印结果我们看到其中的成员。
让我们添加些内容再来观察一下:
1 |
|
看到我们刚才定义的成员属性和成员方法也都在列了。我们还可以实例化之后获取对象的成员:
1 |
|
当我们实例化一个对象obj
之后,打印发现其成员是空的。这是为什么?
原因就在于,这个方法用处其实是打印其对象的专有成员。我们再来为这个实例化对象创建一些成员再来看看:
1 |
|
可以看到,我们获取了刚才创建的成员属性。
以上,就是我们使用__dict__
获取了类和对象的所属成员,方法为:类/对象.__dict__
。
除了获取所属成员,我们还有其他方法,比如获取「文档信息」, 获取「类名称」, 获取「所在文件名称」,获取「当前类的父类列表」以及获取「当前类的『继承链』。来让我们依次看一下:
还记得我们之前在创建函数的时候可以添加文档吗?
1 |
|
同样的,类当中我们一样可以添加文档内容。然后我们可以通过__doc__
来获取:
1 |
|
同样的,__doc__
不仅可以获取类的文档信息,同样可以获取到对象的。
我们使用__name__
来获取类名称「组成的字符串」,
这个方法无法对对象使用。
1 |
|
__module__
可以用来获取类/对象所在的文件名称
1 |
|
如果其所在文件为当前文件,那么这里就会显示为__main__
。
然后是__base__
,
这个方法是用来获取当前类的父类列表。这个方法有两个版本,一个是__base__
,
一个是__bases__
。这两个方法的区别在于一个是获取继承的第一个父类,一个是继承所有的父类的列表,为了呈现的更明显,我们建立一个继承类:
1 |
|
还有一个就是我们上节课讲过的,MRO
列表,也就是__mro__
方法,用于获取当前类的继承链。
1 |
|
到此为止,我们介绍的就是常用的一些内置成员获取的一些方法。当然,这里不是全部,除此之外还有很多,因为并不是常用,所以这里我们就不多介绍了。
方法的分类
接下来呢,我们来看下面向对象的分类,包括:
- 对象方法
- 类方法
- 绑定类方法
- 静态方法
对象方法
其特征为: 1. 在类中定义方法,含有self
参数 2.
含有self
的方法,只能使用对象进行调用。 3.
该方法会把调用的对象传给进来。
1 |
|
这个方法不能直接使用类直接调用,但是其实也不是绝对的。当我们使用类直接调用的时候,需要传递一个参数,也就是必须要self
有参数可接收。
1 |
|
类方法
类方法呢,和对象方法有不一样的地方,也有相同的地方。两者定义十分相似,不同之处是使用装饰器材:
其特征为:
- 在类中定义的方法,使用了
@classmethod
进行了装饰 - 方法中有形参
cls
- 可以不用实例化对象,直接使用类进行调用
- 会把调用这个方法的类或对象传递进来
来直接看代码理解:
1 |
|
看结果可以看到,我们用类进行调用的时候并没有像对象方法一样传递一个参数进去,这是因为调用的时候会直接传递调用的类给到cls
参数。
而我们说不需要实例化对象,并不是实例化对象不可调用。对象调用也是可以的。
至于什么是「装饰器」,我们以后会详细讲到,这里先记住这种形式就可以了。
绑定类方法
这个方法不传递任何对象和类。在定义的时候,不设定任何的形参:
1 |
|
那么绑定类方法既然没有定义形参,那么这个方法是无法使用实例化对象来调用的。
1 |
|
其特征如下: 1. 在类中定义的方法,不必须设置形参。 2. 只能使用类进行调用。 3. 可以传递任意参数,但是不会将类作为参数传递进来。
静态方法
「静态类方法」和「类方法」相似,也需要一个装饰器。并且,静态类方法也是不需要设置形参的。
1 |
|
那从结果中我们可以看到,「静态类方法」可以使用类和对象进行调用,并且调用的时候不需要传递任何参数。
其特征如下: 1. 在类中定义的方法,使用装饰器
@staticmethod
进行了装饰 2. 可以使用对象或者类进行调用 3.
不会将对象或者类作为参数传递进来
⚠️ 注意:这里我们需要注意的,「静态类方法」只是可以不设置参数,并不是不能设置参数,并且,就算是设置了参数之后,也是不接受类和对象作为参数传递的。比如:
1 |
|
我们分别用类和对象进行了调用并传递了两个参数进行打印,而打印结果正常,并且没有对象或者类被传递。
相应的,「绑定类方法」也是这种特性,只是「绑定类方法」只支持类调用,不支持对象调用。
常用函数
其实在之前,关于「常用函数」我们已经接触过了一些,比如:issubclass(子类,父类)
。有些小伙伴可能还记得,这个函数是用于检测一个类是否为另一个类的子类。
那除了这个之外,Python 中还有很多其他的一些针对类和对象的常用函数,下面让我们来详细看一下。
isinstance(对象,类)
,
用于检测一个对象是否是该类或者该类的子类的实例化结果。
1 |
|
这个结果显而易见,那么我们思考一下,既然D
类继承了B
类和C
类,那么obj
对象是否也是B
或者C
的实例化结果呢?
1 |
|
可见,对于继承了父类的子类,其实例化对象和父类之间也会被检测为True
。
hasattr(对象/类,'成员名称')
,
这个函数是用于检测类/对象是否包含指定名称的成员。
1 |
|
在这段代码中,我们给父类B
添加了一个成员属性name
,因为obj
是D
的实例化对象(之前的代码中)。而D
类是继承自B
类的,所以自然obj
中也是包含了name
这个成员属性的。所以我们的检测结果必然为True
。
来,我们做另外一个实验:
1 |
|
我们重新用B
类实例化了一个对象objB
,
然后我们给D
类添加了一个成员属性age
,并且打印了一遍证实其存在。这个时候我们检测了一下objB
中是否含有age
,
因为D
为B
的子类,它说添加的成员属性为独有属性,并不会更改到B
类里,那自然B
的实例化对象objB
中是不可能存在这个成员属性的,结果自然为False
。
``
getattr(对象/类,'成员名称')
,
用于获取类/对象的成员的值
那这个函数就好理解了,我们可以用之前建立好的实例化对象objB
和obj
来获取一下试试看:
1 |
|
没问题,结果如我们所料一般。那如果是获取objB
中的age
的值会如何?我们前面已经知道,objB
中并未存在age
这个成员属性,所以必然会报错:
1 |
|
setattr(对象/类,'成员名称','成员的值')
,
这个函数用于设置类/对象的成员的属性值。
1 |
|
如结果所见,这个方法的返回值为None
,但是我们通过打印obj.name
可知,方法确实更改了obj
中name
的值。
delattr(类/对象,'成员名称')
这个函数可以删除类/对象的成员属性,和del
直接删除对象的成员是一样的结果。
1 |
|
可见,这个方法也是没有返回值的,返回了None
。
不过,既然我们已经删除了obj
中的name
,
为啥还能打印出张三
呢?有没有小伙伴知道为什么?
其实,我们删除的name
是之前使用setattr
为obj
设定的专有成员,当它被删除之后,我们的obj
的继承类D
中还存在着name
这个成员属性,所以现在打印出来的张三
是从D
类中继承过来的。
那如果是我们新添加的但是其他类中没有的属性就会直接报错了,来看:
1 |
|
我们分别打印了两次,第一次setattr
了一个成员属性size
,并且打印验证了。然后我们执行delattr
,删除了刚才设置的成员属性size
,这次再打印来看,报错了。
dir()
这个函数可以获取当前对象所有可以访问的成员的列表。正好,让我们来看看是否从还存在从B
类中继承的成员属性name
:
1 |
|
可以看到打印结果的最后面,确实还存在着name
这个成员属性。
以上函数在讲解的过程中,我们使用的都是成员属性,而成员方法其实是一样的。因为这几个函数说针对的对象都是「成员」。
另外需要注意的一点是,以上所有这些常用函数,都是在可访问的情况下才可执行。我们还有一些不可访问的情况,比如说「私有成员属性」,这种成员是无法被访问或者操作的,我们随便拿个函数来举一个例子看看:
1 |
|
可以看到,当我们意图用getattr
来获取实例化对象obj
中的__sex
属性时报错了。无法正确访问。
1 |
|
当我们使用dir()
来查看的时候,其中也并没有__sex
这个成员属性,有的只是类的私有成员属性:_D__sex
,
我们需要借助类从内部才可访问到:
1 |
|
这样是可以的。
魔术方法
我们在这节课之前,讲到过「魔术方法」, 那我们已经了解,魔术方法是不需要手动调用就可以自动执行的方法。
那我们之前已经讲解过__init__
,这是一个初始化方法。然后还有一个__del__
方法,是一个销毁方法。
这两个方法除了功能上的不同之外,还有一个最大的不同点就是被触发的机制是不一样的。其实,魔术方法中,最重要的一点就是要了解方法的触发机制是什么。
让我们先列出来常用的魔术方法,包括其触发机制,作用以及参数等等...
__init__
, 初始化方法,*****
触发机制:当实例化对象之后就会立即触发的方法
作用:为当前创建的对象完成一些初始化的操作,比如:成员属性的赋值,
方法的调用, 打开或者创建一些资源等等。
参数:一个self
,
接收当前对象,其他参数根据需求进行定义即可。 返回值:无
注意事项:无
__new__
,构造方法,****
触发机制:实例化对象时自动触发(在__init__
之前触发)
作用:管理控制对象创建的过程
参数:一个cls
接收当前类,其它参数根据初始化方法的参数进行决定
返回值:必须返回object.__new__(cls)
进行对象的创建,如果没有返回值,则实例化对象的结果为None
注意事项:
__new__
方法的参数和__init__
方法的参数要保持一致,除了第一个参数。必须返回object.__new__(cls)
进行对象的创建,如果没有返回值,则实例化对象的结果为None
应用场景:设计模式中的单例设计模式。
__del__
,析构方法,*****
触发机制:当该类对象被销毁时,自动触发
作用: 关闭或释放对象创建时打开或创建的一些资源
参数: 一个self
,接受当前的对象
返回值:无 注意事项: 无
__call__
,***
触发机制: 把对象当作函数直接调用时自动触发
作用: 一般用于归纳类或对象的操作步骤,方便调用
参数:一个self
接收当前对象,其它参数根据调用需求缺点
返回值:可有可无
5.__len__
触发机制:
当使用len
函数去检测当前对象的时候自动触发
作用:
可以使用len
函数检测当前对象中某个数据的信息
参数: 一个self
接收当前对象
返回值:必须有,并且必须是一个整型
注意事项:len
要获取什么属性的值,就在返回值中返回哪个属性的长度即可
6.__str__
触发机制:
当使用str
或者print
函数对对象进行操作时自动触发
作用: 代码对象进行字符串的返回,可以自定义打印的信息
参数:一个self
,接收当前对象
返回值:必须有,而去必须是字符串类型的值
7.__repr__
触发机制:
在使用repr
方法对当前对象进行转换时自动触发
作用: 可以设置repr
函数操作对象的结果
参数: 一个self
,接收当前对象
返回值: 必须有,而去必须是字符串类型的值
注意:正常情况下,如果没有__str__
这个魔术方法,__repr__
方法就会代替__str__
魔术方法
8.__bool__
触发机制:
当前使用bool
函数转换当前对象时,自动触发.默认情况下,对象会转为True
作用:
可以代替对象进行bool
类型的转换,可以转换任何数据
参数: 一个self
接收对象
返回值: 必须是一个布尔类型的返回值
以上,我们把常用魔术方法都列出来之后,然后我们来些代码进行讲解。让我们先创建一个Person
类,然后在其中协商构造方法,初始化方法和析构方法。
1 |
|
当我们完成实例化的时候,「构造方法」先是用*args
接收了所有传递的参数,并且使用存储了元组。我们可以看到,**kwargs
什么都没接收到,所以打印为空。
当「构造方法」执行完之后,也并没有去执行「初始化方法」和「析构方法」,这又是为什么呢?这是因为如果在「构造方法」中没有返回对象,这对象无法创建。要想对象进行创建,这我们必须返回object.__new__(cls)
进行对象的创建。这在之前「构造方法」的说明里有说明。这个cls
参数是什么呢?我们直接来看代码:
1 |
|
现在可以看到,我们打印了cls
,
实际上就是Person
这个类。当我们返回object.__new__(cls)
之后,可以看到__init__
初始化方法正确运行了,执行了方法内的打印方法。然后最后,我们打印了zs
这个实例化对象。那为什么__del__
析构方法没有触发?因为我们是在Jupyter
中执行,并未执行释放,此时我们如果del zs
,则会触发析构方法,或者,我们讲上述代码保存为一个22.py
文件,然后单独执行,这个时候
Python
的垃圾回收机制会执行,就会进行释放,从而触发析构方法。如下图:
我们接着上面写的代码在22.py
中继续写:
1 |
|
报警,告知我们这个类当中没有
cllable。那如果我们讲这个类改造下,加上__call__
:
1 |
|
这样,我们直接执行zs()
就没问题了,可以把对象当作函数直接调用时自动触发。
让我们继续,返回到22.ipynb
笔记本文件中,让我们重新定义一个类:
1 |
|
报错信息中可以看出,这个实例化对象是没有len()
方法的。我们如果给它加上__len__
之后就会让其拥有len()
方法。
1 |
|
因为当前我们在类中定义的items
里面没有数据,所以返回的长度必然也是0
。但是这个返回值是必须要有的,需要返回一个整型才行。
来,我们看看如果这个方法的返回值写死会如何:
1 |
|
很明显,我们重新给obj.items
进行了赋值,目前其长度是7
,
可是返回值依然是1
。说明__len__
的返回值只要四个整型就行。那我们就需要注意了,len
需要获取什么属性的值,就在返回值中返回哪个属性的长度即可。当我们用正确的方式返回的时候就会是这样:
1 |
|
让我们继续接着这段代码来玩:
1 |
|
发现没有,虽然我们使用了str()
方法,可是最后返回的结果,和直接打印obj
的结果是一样的。那为什么会这样呢?这是因为我们这个当前的方法其实是对obj
对象进行了一个转化字符串操作,而其本身就返回了一个<__main__.Demo object at 0x110631270>
的字符串。
其实这个返回的字符串我们也是可以自定义的,
使用__str__
方法给一个返回值就可以了。
1 |
|
我们直接打印了obj
对象,因为__str__
方法的存在,所以现在直接打印了返回的字符串。也就是说,该方法可以代替对象进行str
或者print
的字符串信息返回。
继续来看:
1 |
|
可以看到我在类中注释了__str__
方法,这是因为只有其不存在的情况下,__repr__
方法才会起作用,可以替代__str__
方法。
那么到底__str__
和__repr__
两个到底有什么区别呢?让我们直接在代码里找答案:
1 |
|
这个时候两个的结果都是一样的,似乎并看不出两者到底有什么区别。别急,让我们继续往后做这个实验:
1 |
|
两者的类型都是一样的,返回了一个字符串类。难道这两者就正的毫无区别吗?Python 得创建者吃饱了撑的没事做两个功能一模一样但是名字不同的方法?
1 |
|
仔细看,两者似乎有了细微的差别。repr
解析的结果带着引号。
那么,str
和repr
函数都可以把其他类型的数据转为字符串类型。
str
函数会把对象转为更适合人阅读的形式,repr
函数会把对象转为解释器读取的形式。
如果数据对象并没有更明显的区别的话,str
和repr
的转化结果还真没什么区别。
这两者的区别,其实只要了解一下就可以了。大部分时候,并不需要那么较真。
接着让我继续来看看__bool__
:
1 |
|
当我们对obj
使用bool()
方法的时候,返回值为True
。
那说明其中包含了一个bool
机制,并且默认返回值为True
。
这个时候让我们来定义一下看看:
1 |
|
由于我们的items
中设置为空值,而我们将前面定义obj
的items
的那段代码删掉了,所以这个时候,传入方法的self.items
的值也为空,必然返回值就是False
。也证明了,bool(obj)
拿到的返回值就是类里定义的的__bool__
中返回的对象。
介绍完常用的一些魔术方法之后,我们再来看一些其他的魔术方法。同样是魔术方法,为什么我要明显的区别开来讲呢?那是因为现在开始说讲的魔术方法都是针对成员的,是一些成员相关魔术方法。
__getattribute__
: 优先级最高
触发机制:
当访问对象成员时,自动触发,无论当前成员是否存在 作用:
可以在获取对象成员时,对数据进行一些处理 参数: 1.
self
接收对象,2. item
接收当前访问的成员名称
返回值: 可有可无,返回的值就是访问的结果
注意事项:
在当前的魔术方法中,禁止对当前对象的成员进行访问,会触发递归。如果想要在当前魔术方法中访问对象的成员必须使用object
来进行访问。格式:
object.__getattribute__(self,item)
__getattr__
触发机制:当访问对象中不存在的成员时,自动触发
作用:防止访问不存在的成员时报错,也可以为不存在的成员进行赋值操作
参数: 1. self
接收当前对象,2.
item
接收当前访问的成员名称
返回值:可有可无 注意事项:当存在
getattribute 方法时,会去执行
getattribute
方法。也要注意,不要在当前的方法中再次去访问这个不存在的成员,会触发递归操作
__setattr__
触发机制:
当给对象的成员进行赋值操作时会自动触发(包括添加,修改)
作用: 可以限制或管理对象成员的添加和修改操作
参数: 1. self
接收当前对象 2.
key
设置的成员名 3. val
设置的成员值
返回值: 无
注意事项:在当前的魔术方法中禁止给当前对象的成员直接进行赋值操作,会触发递归操作。如果想要给当前对象的成员进行赋值,需要借助
object
格式:
object.__setattr__(self,key,value)
__delattr__
触发机制: 当删除对象成员时自动触发
作用:
可以去限制对象成员的删除,还可以删除不存在成员时防止报错
参数:1. self
接收当前对象 2.
item
删除的成员名称 返回值: 无
注意事项:
在当前魔术方法中禁止直接删除对象的成员,会触发递归操作。如果想要删除当前对象的成员,那么需要借助
object
。 格式:
object.__delattr__(self,item)
好了,按照惯例,让我们上代码, 先让我们来定义一个最正常的类,并且实例化它:
1 |
|
这个时候,让我们在类中定义一个方法:__getattrbute__()
。
1 |
|
可以看到,虽然我们在实例化对象的时候传入了成员值,但是当我们打印的时候返回值为None
。如果我们这个时候修改一下这个魔术方法:
1 |
|
那我们拿到的就是得到的内容。不仅是name
,
任意我们传入的成员,返回的值都为__getattribute__
返回的值。当获取对象成员时,这个方法进行处罚,其中的item
形参就是我们想要获取的成员属性。第一次是obj.name
,
第二次是obj.sex
,但是无论你调用的是什么成员,拿到的都是这个方法的返回值abc
。
那既然这样,是不是我们返回对象的成员属性就可以了?
1 |
|
千万不要这么做,这样会引起方法的无限递归调用,最终导致栈溢出。那么是不是我们就没办法了?也不是,我们需要使用object.__getattribute__(self, item)
:
1 |
|
这样,我们就获取到了正确的返回值。我们这里讲解一个__getattribute__
方法,限于篇幅的原因,我们其他的几个方法就不细致讲了。在我们先前的列表内,每一个方法的触发机制,作用,参数和注意事项我们都有写清楚。大家可以执行去看看,并做一些测试。让我们赶紧进入下一个阶段,不过在这之前呢,我们还是需要讲访问成员的顺序给大家强调一下,这个还是比较重要:
- 调用
__getattribute__
魔术方法 - 调用数据描述符
- 调用当前对象的成员
- 调用当前类的成员
- 调用非数据描述符
- 调用父类的成员
- 调用
__getattr__
魔术方法
以上步骤是调用某个成员时的顺序,前面的能够调用成功,后面则不再执行。至于描述符,咱们下节课来详细讲。
好了,本节课到这里就结束了,让我们先预告一下,下节课呢,我们来讲讲面向对象中的「描述符和设计模式」。大家期待一下吧。
记得课后好好做练习,目前我们的课程稍微有些难度了,只有保持一定的练习量,才能理解并记住。
小伙伴们,下节课再见了。下课。