手把手教你学python第十五讲(魔法方法续私人“定制”)

如果看不了图,请去https://www.bilibili.com/read/cv317161

python无处不对象的深刻理解

前面写了这么多,我觉得有必要从一个大的层面,也就是OO来看问题的本质。只要你调用对象的语法是合乎python的习惯的,那就是可以的,我们以前从来没有像下面这么写过,对吧,但是仔细想想有何不可呢?一个类定义完了就是一个对象啊,我当然是可以改变一个对象的属性,只要语法结构合乎python的习惯,我们下面就是把方法当作普通函数去调用,只是因为它是类定义里的函数,所以前面加上a.b(a),这个.就是python的一种规定,或者说开发人员的源代码就是这么写的,其它的和一般的函数调用没有什么区别。大家不要学死了,python无处不对象,一定要记住这句话。

t

实例化对象的绑定方法的调用只是有一种简便书写格式而已,以下图为例,a1.b()其实相当于从a1.__class__.__mro__里面去找b方法,并且把a1作为参数传递给方法。

我们甚至还可以这么写,我用通用写法b.c(a1),都是没有问题的,其实self只是个形式参数而已,你可以用任何合法字符,写成self算是一种规定吧。结合上面理解一下,下面还会讲这个问题

继承的本质

前面我们曾经说过继承,都是各种覆盖啊,什么东西,下面我会以一种可视化的方法,一种更为简单的方法让你来理解继承。我相信会对你有帮助的


其实访问d1.n就相当于下面的代码

所以说继承的本质是什么?拿上面例子来说,其实就是新建一个d1.__class__.__mro__列表,访问实例化对象的属性的时候python会先在d1.__dict__找,然后在这个MRO列表里依次寻找.__dict__有没有你要找的属性,没有报错,有的话就返回第一个找到的属性。所以现在可以把脑里那些什么各种继承的属性标签的覆盖都忘了,就按照这个顺序来找,我们就再来说说这个__dict__,它其实就是一个字典存放与你这个对象相关的一些东西,对象是类对象的话就是类定义里定义的属性和方法,如果是实例化对象就是绑定属性,实力化对象是没有方法的,方法都是归属于类的,这个前面也说过,我们就再用这个__dict__去理解一下属性和方法覆盖的问题,注意看下面__dict__的不同

我们用__dict__来理解继承的机制

b继承了a其实相当于什么?我的理解是b继承a其实就像我们在b网站上有一个a网站的链接,我们就把__dict__理解为网页上的内容,b网站可以有和a网站上一样的板块,比如说python版块,但是如果我们在b网站上没有找到我们想要的版块(这是可能的吧),这时候就体现了继承,我们点开b网站里a网站的链接去到a网站去看有没有,如果还没有就继续点链接,__mro__其实就是链接的一个顺序,当然我们这里假设每一个网站只能有一个链接向其它的网站,假设b网站没有python版块,我们就要点开链接去a网站看有没有,如果有我们实际上是在a网站看的python。如果b网站原来有c语言版块,我们每天都在b网站看,某一天突然这个版块被删了,那么我们就只能点开链接去a网站看了,a没有就继续点开链接。我们再来理解一下实例化的过程,其实实例化是什么呢?前面说过类就是图纸,类的实例化就是照着图纸去盖房子,现在我们要说了这个房子其实是个假房子,为什么这么说呢?就看下面的例子,因为a1作为a的实例化对象以后,a1.__dict__是空的,但是为什么访问a1.n返回了1也就是a.n呢?其实这个上面已经讲过,就是按照__dict__和__mro__的一个顺序去搜索,这里我找不到很好的比喻。我们只有发生了赋值行为,就相当于进行了装修,__dict__里才会有内容,绑定方法其实就是这么一种赋值行为。当然python里的赋值和c语言的赋值含义是不一样的,学python的有时候会说这么一句话,python没有变量只有名字,关于这部分内容前面已经介绍过不少了,包括浅拷贝和深拷贝的内容,不熟悉的请去前面看。

不知道你们还记得不记得前面提到过一个静态变量,回顾一下

为什么删除了a,我们还可以调用a1.n呢?前面我们有一种解释是还有a1.n这个地址指针指向它对吧,但是结合这里我们认识到本质其实还是按照静态变量来理解,静态变量的寿命是程序的全周期,即打开到关闭IDLE或者其它编译器的时间,编译器不关闭,它就一直占用内存,当然还是说你觉得怎么简单怎么理解。上面好像没有涉及到方法的调用,只是说属性是按照__mro__的顺序,其实方法也是,只是第一步不需要搜索实例化对象的__dict__而已。

没错你看到了c.b显示的是function a.b如果你理解了上面的讲解不难理解。

我们重新分析一下a1+5和5+a1结果不同的原因,首先这种双目运算符,先去找两边对象的最底层的类,上面的例子也就是a,上一讲已经说过+是自动先调用__add__方法的,如果__add__不适用,什么叫做不适用呢?其实就是isinstance(a1,a)是False,就__radd__方法,a1+5,a1是a的实例化对象,所以按照a.__add__去找,a.__dict__里面没有__add__,于是就到a.__mro__的下一个int去找,调用了int.__add__。那么5+a1呢?isinstance(5,a)返回的是False,而且a.__dict__里面有__radd__,于是就调用了。

总结一下这种自动调用是这样的,先判断isinstance的真假。决定是调用__add__还是__radd__,确定了之后就开始调用,如果不是自动调用就没有isinstance这个过程了,如下

我们这里再来理解一下实例化对象绑定方法的简便写法的实质,b1.b()其实就当于b1.__class__.b(b1)


其实这种简写的本质就是在内存里给这中符合python书写语法的函数开辟了一个新的内存空间来存放这种绑定方法,每实例化一个对象就开辟一个空间。和上面是不冲突的,只是这是一种特殊的写法而已。

那么我们来看关于上一讲的最后一种super(basetype,type),它是可以调用绑定方法的

只需要在后面传进去参数。还有一点需要注意,用super调用父类方法和直接调用父类方法的写法是不同的,注意super函数self参数的位置

为什么会报错呢?其实仔细想一下就会发现,self已经在前面给过了,所以报错说期望得到一个参数,但是给了2个,我们改一下就ok了。

关于属性的魔法方法



也就是说a1.n就相当于先后调用a.__getattrribute__和a.__getattr__,因为是先打印1,再打印0,a1.n=2就相当于自动调用a.__setattr__(a1,n,2),del a1.n就相当于调用a.__delattr__(a1,n)。我这里要提醒一点,防止出现无限递归,也就是不要出现这种程序

其实想一想就知道就知道为什么,a1.n=2本身就是调用__setattr__方法本身,当然是无穷递归咯。我们来稍微写一个小程序吧,来定义一个矩形类,如果给属性square赋值,那么它就自动的让长等于宽,如果不是,就输入长和宽

程序本身不难,就是体会一属性魔法方法的使用

这里我还要讲一点,虽然以前我已经讲过这点了

报错的原因是super没有属性c,是不是让你以为是super 函数的问题?但是其实不是的

super没有__add__方法为什么不报错?其实这里应该是super()的问题,对于报错的其实就是object的问题,因为a.__mro__里a后面就是object,我们看object是没有__getattr__方法的,而super(a)其实就是int,int是有__add__方法的,这点希望你们不要误解。不知道你们还记得不记得我们前面还学过一些函数来得到属性的

其实它们也是在内部调用了这些魔法方法。是不是还有一个呢?没错还有一个property,下面参考了http://bbs.fishc.com/thread-51106-1-1.html,但是有我自己新的理解

,没错,property是一个类,它并不是一个函数

这种类我们称之为描述符,什么是描述符?某种类型是说有下面三种魔法方法的类

说详细一点的话

我们先来看看这三个魔法方法,首先要说明的是这三个魔法方法不像__new__,__init__这种每个类都会有,而只是特殊的描述类才会有,比如说property,当然我们可以自己写这些魔法方法


我们再来掌握一下这些魔法方法各个位置的参数是什么含义。看下图我们很简单就可以理解为什么说描述符是将某种类型的实例对象指派给另一种类的属性。因为b类里我们看到了n=b(),n就是b的一个属性,而a()是a类的一个实例化对象,而b类的属性n就作为标签或者说指针指向a()。然后我们调用了b.n,这时候其实自动调用了a类里的__get__,看到先后打印了a的实例对象,None,b类。说明self就是a()也就是b.n,instance是None,owner是b。这是直接调用,然后我们实例化了b类,产生了b1实例化对象,下一行给b1的属性s赋值2,并没有发生任何事情,因为属性并不是n。接下来给b1的属性n赋值3,自动调用a里的__set__,打印了a的实例对象,b的对象,也就是说self是a(),也就是b1.n,instance是b1,其实到这里我们能看出些端倪了,如果没感觉,没关系,下面我们又访问了b1.n,自动调用了a.__get__,打印了b.n,b1,3。我来解释一下,其实b1.n=3代码相当于a.__set__(b1.n,b1,3),在这个__set__里我们改变了self.value也就是a().value的值,然后b1.n就相当于

a.__get__(b1.n,b1,b),注意b.n=a(),你应该知道对应关系是怎样的了。为什么上面的b.n

第二个是None呢?很简单因为没有b的实例对象。还有下面b.n=3之后为什么什么都不会发生了呢?因为b.n已经不指向a()了嘛,后面的代码都很好理解,只要你看了前面所有的系列。

这几个参数不要随便变都是有固定的格式的

上面为什么在括号里写b1.n而不写a(),这是因为写a()的指针可以避免下面的问题

没有指针指向实例对象,每次a()都相当于创造了一个新的实例对象,虽然它们很快就会被垃圾回收机制回收,但是id还是不一样的,但是你看到上面的例子里a的object的地址全是0x013374D,所以直接写指针b.n。但是不知道针对这点你们会不会想到什么问题,既然b.n是一样的,那么如果我有b的两个实例对象b1,b2,我先b1.n=2,在b2.n=3,b1.n会变吗?


看来我们会死杞人忧天了,其实仔细想想也能理解,因为还有一个参数instance呢,另外从上面还可以看到,b1.n与b.n都是在0x019A76F0,b2.n是打印0x019A7390,变了对吧,这也可以理解为什么会出现上面的结果,每一个b的实例对象创建出来,a()都会执行一次,也就是说会有一个新的a实例对象,就拿上面来说0x019A76F0是和b.n和b1.n有关的,而0x019A7390是和b2.n有关。还有b的第一个实例对象b1.n和b.n指向的地址是一个地方,并且我们让b1.n=2,b.n的值跟着被改变了。我现在想删掉b1.n,唉报错了,因为我们没有定义a.__delete__,删除b.n为什么可以?因为它只是b的一个属性而已对吧,完全没有问题,但是为什么b1.n报错,就是因为这里面a是描述符,而del b1.n就是会自动调用a.__delete__但是我们没有定义,所以报错,注意上面说过除了property有这个魔法方法以外,你自己定义的类的__delete__需要你自己去写。

如果你前面学的融汇贯通了,下面代码你会一看就懂是怎么回事,就是在类定义外面增加了属性而已

需要注意的是这个属性必须是类层次的属性,不能是某个实例对象的绑定属性。

至于为什么会出现这样,我不是很了解,毕竟我们连__get__,__set__,__delete__三个魔法方法的源码都不知道,我们只是会用。但是其实有这种骚操作

这种操作其实每次实例化一次b,b.n=a()都会执行一次,也就是会说每实例化一次,b.n都会随着实例对象变一次,这就是b2.n=3之后b.n也等于3的原因,你可以去和上面在类层面上的属性对比,最后的结果是b.n=2。体会这种不同。我们来试着编写一个小程序体会一下这种编程思想。


我觉得这个程序你们应该尝试自己去读懂,为什么有的是self有的是instance?一定要自己读懂,我在每个魔法方法里都加了一些标记,这会帮助你读懂它,我不会讲解了,我希望你能思考让脑子动起来。


说了这么多我们是不是还没说到property。这里就是,参考了http://bbs.fishc.com/thread-51106-1-1.html。里面说的是有错的,首先上面我们就知道property并不是个bif,严格来说是一个类。再来看一遍property的帮助

我没有太多项目经验,对它的理解不是很到位,所以这里摘一段话解释property的作用

上面已经有个典型例子。细心的人会发现为什么x前面要加一个_x?只是为了和x区分而已,这里的类属性和绑定属性不能重名的,不然会无限递归


为什么会无限递归,其实也很好理解,就是因为重名的问题,a1.n=4我们进入了a.b(a1,4)

里面又是a1.n=4,又调用了a.b(a1,4)于是就无限递归了,不重名就不会出现了。这是需要注意的一个点

有了上面的知识你能不能尝试把property这个类写出来呢?只要你懂了上面说的内容,这个应该不是什么难事。

里面加的print都是为了看清楚这个过程

我们现在已经把property给剖析得很深了,不知道你们是否还记得,上面帮助文档里还有一个修饰符的例子,


其实就相当于

只不过也许你们发现了用修饰符修饰的写法方法名一样,参数不一样,我们来试一下






格式比较固定,唯一可以变的就是@x.setter和@x.deleter的位置,修饰._x的方法名字必须一样,还有修饰符前面也必须一样。到这里描述符就介绍完了。下面介绍一下定制容器

定制容器

什么叫容器呢?前面讲过的列表,元组,字典,集合,字符串都是容器类型。而其中列表,元组,字符串我们称为序列,它们是有顺序的,可以通过索引值访问。而字典集合是哈希的

,是没有顺序的,不叫做序列。容器类型就是存储数据的嘛。首先谈一下协议


也就说是一种规定而已,但不是绝对的,不是严格的。就比如说我们有frozenset和set,set是可以改变内容的,而frozenset不能,根据自己的需要来设定。

我们先来体会一下

元组类型是没有__setitem__,__delitem__这些魔法方法的,这是python自己写好的元组类型,如果你希望元组有这些魔法方法,你可以自己去定制一个元组。下面我们演示一个如何

定制一个不可以被改变的列表,按照这种思想,你完全可以去定制可以被改变的元组,你只需要添加__setitem__,__delitem__魔法方法就好。


如果你不记得fromkeys下面会帮助你理解


结果是ok的对吧。还有最后的几个简单的魔法方法在下面,就不演示了,因为比较简单,前面也已经演示过很多的魔法方法了。


一个小建议

其实作用和C语言里宏定义一样,有了BaseAias=BaseClass,以后如果BaseClass的名字换了,我们只需要改变一个地方就够了。本讲代码都不长,请自己打吧。


猜你喜欢

转载自blog.csdn.net/qq_41740705/article/details/79678929
今日推荐