手把手教你学python第十二讲(类和对象收尾)

如果图片刷不出来,请到https://www.bilibili.com/read/cv278779

为了讲下面的内容,有必要加深你们对上一讲中类,类对象和实例对象的区别,我在这里再举个例子


实例化a后,我们有c,d两个实例对象,我们看到起初a.b=c.b=d.b=0,这是因为在a类定义时设定的b属性的默认值就是0,我们让c.b+=2之后c.b=2,而a.b=d.b=0,但是我们让a.b+=1,a.b=d.b=1,c.b还是等于2,这是为什么呢?因为类的实例化会自动生成属性的默认标签,如果实例化对象如果重新定义了这个属性,python就会给这个实例化对象的属性一个新的标签去覆盖原来默认的标签,或者说默认的标签被屏蔽了(我们前面就说过在python里变量就是标签,其实这有点像函数里出现的变量如果和全局变量重名,python会自动屏蔽掉外面的全局变量)。c.b+=2就是这样,c这个实例化对象的b属性新的标签把默认的b属性的老标签覆盖了这里的覆盖是指时间上的覆盖而不是空间上的覆盖,空间上的覆盖是必须在同一个id上覆盖,而时间上的覆盖可以在不同的id上,举个例子

大家注意看上图哈,前面说过实例化的时候会自动生成一个默认标签,这里实例化a1的时候就生成了一个默认的a1.b标签这个标签是贴在id505991632上的,然后下面我们重新定义了a1.b=3这时候原来的标签没有消失,它只是被时间上后来的a1.b=3这个标签覆盖了,为什么说是时间上呢?因为它们贴在不同的id上嘛。紧接着我们del了a1.b,我们这里就必须讲一下del这个函数了,它的作用是就是撕标签,不会改变对象的内容,就只是撕标签。这里就又必须补充一下撕标签和删除内容的区别

上图中b=a是深拷贝,看过这个系列的应该知道什么叫做深拷贝哈,然后我们del a,撕了这个a的标签,再访问a当然会报错,我们再访问b,可以访问对不对,而且id还没有变的。重新一次定义a,b我们看到id是变了的,python开辟了一块新地方来存a这个列表,这里再一步拓展,python里数类型的变量,比如存的是一个整数,或者可以用ASCII码存的字符串,也就是只能出现ASCII码表里出现的字符。它们只要值是一样的话,就会存在同一个地方,比如

即使没有加b=a这样的语句,它们只要内容列表一样,就相当于是深拷贝,而浮点数,包含不在ASCII码表里的字符的字符串也包括汉字,汉字在python里是utf-8编码,列表这些如果不用赋值语句即使内容一样也不构成深拷贝,不会存在一个地方的


我们继续讲解clear,

看到a的id是26934928,然后a.clear之后a,b都变成了空的列表,而id还是没变,说明del只是删除标签,而列表的内置方法clear是可以改变内存空间的存的内容的。话又回到覆盖上来

b

把a1.b这个标签撕了之后,我们又调用了a1.b,发现还能调用,这是为什么呢?因为为前面说过,在实例化的时候,自动会生成一个默认的a1.b标签,我们后来又重新定义了a1.b,又给了它一个新的标签,老标签在时间上被覆盖了,也就是说其实这个时候a1.b是有两个标签的,一个是实例化时候的默认标签,另一个是重新定义时新加的标签,这两个标签之间是覆盖而不是取代,覆盖是同时存在,只是python只看到了时间上后产生的标签,什么叫取代呢?我载举个例子

我们看到del a一次,a标签就撕了,但是同样我们也无法访问a()了,这就是取代,a=1这个定义产生的标签直接把前面的a()函数对象的标签直接取代了,只能同时存在一个标签,这就叫做取代。继续说覆盖,a1.b的重新定义的标签被del撕了以后,a1.b的默认标签就可以被python看见了,因为没有标签在它上面了对吧,所以说a1.b就跟随着a.b的值,因为默认标签在实例化的时候就是从a类定义里深拷贝过来的,python暂时看不到老的标签,而d.b还是原来的默认标签(这个默认标签跟随a.b,a.b贴在哪个id上,默认标签就贴在哪个id上,就相当于有一条绳子把d.b的默认标签和a.d连着),所以就出现了c.b动而d.b和a.b不动的情况,因为c.b已经不是原来的默认标签了,它不是你a.b的跟屁虫了,同样,a.b和d.b也不跟着c.b变,因为c.b已经从d.b和a.b的家族里离家出走了,c.b已经自立门户了。这也可以解释为什么后面的a.b+=1,a.b=d.b=1,c.b还是等于2。上面一讲中说过,属性和方法重名时,写在后面的会覆盖写在前面的。解决办法如下

注意继承仍然要注意不要和父类的属性和方法重名,因为重名还是会覆盖掉,而组合是没关系的,因为没有继承关系。

我这里再说一个最基本的东西,别让变量名和类名重了

Mixin

首先介绍一个Mixin是什么?


组合

下面是个例子,把t类和f类的实例化都放在p的定义里,假如t就是乌龟,f就是鱼,p是池塘。输进去乌龟和鱼的个数,打印出来。

前面讲过,实例化一个类的时候,比如T=t(x),t的类定义里__init__方法的第一个参数自动替换为T,也就是说这一句的操作是T.x=x,那么下面的代码你们应该也会读了。我在这里再说一点,python是区分大小写的哈。

但是不是绝对的好吧。如果你们还记得我上一讲最后写的点和线两个类的程序的话,你会看到‘’线上有点“”,也是可以用继承的,同样上面的代码也是可以用继承的。

但是这样写需要注意有可能必须忽略掉父类的__init__方法,这主要还是看父类方法中操作和子类冲突不冲突。还有一种

但是我要揭穿上面继承的真面目,其实这是一个假继承,因为除了pass掉__init__方法这个操作之外(因为子类继承了父类的__init__方法,当父类的初始化方式对子类不合适的时候,我们要pass掉__init__方法,自己重写),其它任何操作都和组合一样,只是在别的方法里组合了而已,你们可以去和上面组合的代码对比。这说明实现相同的操作,这种伪继承效率不如组合,所以遇到可以用组合的时候直接用组合就行,不用犹豫了。

在这里补充一个上一讲没有讲到的知识,

如果多重继承里的父类还有继承关系,那么必须子类写在前面,上一讲有讲过的写在前面优先级高是对于没有继承关系的来说,而有继承关系的应该是c->b->a这么一个顺序不可以反过来。

需要记住这点,多重继承的时候,子类必须写在父类前面,没有继承关系的时候顺序影响优先级,调换位置改变优先级,不会报错。多重继承还会有下面的报错

继承关系如下图

为什么图里有两个A1?因为B1,B2在继承A1和A2的时候写的顺序不一样,你可以去上面看,如果改为下面的代码,就可以了

这时候的图就变成了

细心的朋友会发现有一个词MRO总会出现,MRO是方法解析顺序,是根据C3算法得到的一种顺序。这里我不会展开去讲了,这个东西和图论是有关系的,对于初学者来说,只需要小心翼翼的写代码,最好画出来这个图,怎么画这个图呢?其实也和MRO有关系,我告诉你一个简单的原则,最底层的子类作为根,把括多重继承括号里写在左面父类的画在左边,写在右边的画在右边,而且要保证这个左右关系(这个保持左右关系和子类的位置没关系,只对父类起作用。比如说一个a继承了b和a继承了b,c分别可以如下画

)而且必须是单调的,什么叫做单调呢?。我们以上面为例


我们就理解到这里就行,我只是简答地告诉你这么做,其实对于新手就够了,如果你还想探究下去,推荐三个网站给你http://blog.csdn.net/langb2014/article/details/54800203

http://python.jobbole.com/86787/

http://python.jobbole.com/85685/

里面还有super的详细说明,如果你还记得,我们上一讲就提到了super函数,你可以去看看它的作用。

__bases__

如果你看了http://bbs.fishc.com/thread-48888-1-1.html,这里面是有错误的,在python3里是行不通的,因为python2和3差别还是挺大的。

我们看到__dict__是不支持赋值,也就是不能通过__dict__改变属性的值,至于什么叫做mappingproxy,可以看https://stackoverflow.com/questions/32720492/why-is-a-class-dict-a-mappingproxy。至于__bases__,是的,这是目前python3的一个bug你可以去https://bugs.python.org/issue672115看看。可能如果你的版本高这个bug会修。

__dict__

首先我们要知道什么叫做绑定。

其实绑定很简单,我们来对比代码

绑定分为两种,一种称之为类对象的绑定,这种很简单,只要在类定义里出现的属性和方法,都自动和这个类对象绑定。而类的实例化对象的绑定必须是大多数都需要经过绑定方法,什么叫绑定方法呢?只要类定义的方法有参数这个方法就叫做绑定方法。类的实例化对象只能调用绑定方法,调用非绑定方法会报错。上面的a类定义里的b方法是带参数的,因为是def  b(s)这个方法的定义里b是有参数的(方法和函数是有一定区别的,不清楚的看上一讲),那么a1可以调用这个b方法,不会报错,绑定方法里可能会有绑定属性。而如果是def b(),实例对象a1是不能调用这个未绑定方法的。只能通过这个a类或者a的子类来调用(如果子类里没有出现这个b方法的定义)。类对象的绑定属性和方法和实例化对象的绑定属性可以用__dict__来查看(实例化对象不显示方法是因为方法是类对象的,不是实例化对象的,实例化对象去掉用绑定方法实质是什么?就是把实例化对象作为参数传进方法里,所以说方法并不是实例化对象的,而是类对象的,如果不理解下面例子会让你明白的)

从上图我们可以看到a的类定义出现过的所有属性和方法可以在a.__dict__里找到,可能属性和方法在a.__dict__里出现的先后和你写的不一样,这很正常,因为有个{},表示它是字典嘛,字典是无序的(字典的排列内部有一套哈希函数转换的机制,不是透明的,所以对于你来说,看到的就是无序的,但是对于计算机,它是有自己一套方法来排序的,只是你不可见)。然后看a的子类b里面只出现了a属性和e方法,这也很正常,前面也说过,只有在类定义里出现过的属性和方法才和这个类绑定,所以你就理解了为什么b.__dict__里只有a属性和e方法了,而没有父类a的b属性和c,d方法,因为它们没有出现在b的类定义里,没有绑定。然后我们看实例化对象c一开始c.__dict__是空的,因为你还没有调用绑定方法,获得绑定属性。然后我们调用了c方法,其实c.c()等价于下面的写法,只是python认可上面的写法,有的时候可以减少你打的字母


c这个实例化对象调用了绑定方法c之后,就获得了c.x这个绑定属性。就可以c.__dict__出来了,所以,实例化对象获得绑定属性是通过绑定方法获得的。作为a类的实例化对象c,在实例化的时候就会有c.c方法这个标签了,这一点上面说过,但是c.a,c.b没有和c绑定,因为没有经过绑定方法,所以在c.__dict__没有找到。这里还要注意一点,绑定还可以通过直接方式产生,什么叫直接方式呢?比如下图中的类对象没有在类定义里面而是直接在IDLE里面打的b.a=1,a.c=0,都在__dict__方法里面显示出来了,还有实例对像a1在IDLE里面直接打了a1.a=2也产生了绑定,在a1.__dict__里面出现了

只有在__dict__里面显示的绑定属性或者方法才可以被del撕去标签,否则会报错。


对于上面两张图,如果你前面的都看了,我相信应该不会对del了a1.b怎么调用a1.b还有值和b.b还可以访问有疑问,覆盖不是取代是吧,是两个都有,如果不懂,返工重新回到上面覆盖那里重学。下面是一个关于静态变量的说明,静态变量可以理解为什么呢?就是贯穿于整个程序的变量,只要不重启编译器,它们的内存空间不会被释放,类定义里面的属性和变量就叫做静态变量。下面就演示

删除了a类之后,又删除了b.b标签,但是为什么还能访问到b1.b呢?可以有两种理解,一种就是静态变量,因为只删除了a类这个标签但是a里面的属性和方法都还在内存里面,只是通过a无法调用,但是可以通过实例化对象调用。还有一种就是说,b1=b()实例化的时候,python自动把b.b的两个标签(一个是继承标签b.b,这个标签跟随a.b,一个是重新定义生成的标签b.b)都自动生成了,del b.b只删除了那个重新定义生成的标签b.b,而继承标签b.b跟随a.b的那个没有被删除,只要有标签贴在内存空间,那么内存空间就不会释放。怎么好理解怎么来吧,但是这个的机理其实就是静态变量哈,这里我不展开讲静态变量,感兴趣自己去搜资料吧。多重继承里面这个还是比较有意思的

会不会有人问?del a.b了但是不是还有一个继承标签c.b指着同一个id吗?这是因为继承标签和默认标签都是跟随a.b的动作的,del a.b其实就是把这些跟随这它的标签也一并删除了,军不可一日无帅嘛,这个元帅a.b标签没了,它的小跟班一起都完蛋了。

下面我要讲的和__dict__关系其实不是太大,但是可以加深对于什么是覆盖和python存储结构的理解,a的类定义和上面一样,

第一个class b(a)里我们让a=0,你去看上面的a类定义里,a属性默认值就是0,我们这里还让a=0,看一看a.a和b.a的id,是一样的,说明它们都贴在同一个id上面,这个上面讲的也很清楚,其实在继承的时候,b类也会自动把父类的属性和方法进行深拷贝,生成继承标签,然后如果在b的类定义里出现了与父类的属性和方法重名的属性和方法,就会生成新的标签去覆盖(上面已经说过什么叫做覆盖)。下面张图没什么深意,就还是说明了如果整形变量的值一样,它们自动地相当于深拷贝,但是一个int和list不同的是list有内置方法clear,而a=1,b=a,a=2不是改变内容,只是把a从一个id撕下来贴到另一个上,这个前面的系列都演示过。

还有一点需要提醒,实例化一定要加上(),不然不是实例化,这叫类的拷贝。

相关的Bif


它是判断第一个参数是不是第二个参数的子类。

第二个参数注意只能用元组的形式。而且它会判断自己是自己的子类,object是所有类的父类,是最顶端的类。没有这个类它会报错,而且第一个参数只能有一个。

假如我第一个参数传进去实例化对象会有什么结果呢?

会报错提示你第一个参数必须是类


判断第一个参数,对象,是不是第二的参数的实例化对象。


第一个参数你填入类对象不会报错,但会返回falase,因为它是判断是不是实例化对象而不是继承关系的,子类的实例化对象也是父类的实例化,这点需要注意,也许你会说上面的b类里面没有任何修改,那我们就修改一下

看到还是True

和属性有关的操作

第一个hasattr是判断对象有没有这个属性,返回布尔值,第一个参数是对象,第二个是属性,必须用引号括起来,如果对象没有属性,会报错。第二个gettattr是获得对象的属性,注意属性要用引号括起来,如果没有这个属性而且default没有设置,就会报错,如果设置了default就会返回default的参数。第三个setattr是设置对象的参数,第二个参数必须用引号括起来,第三个参数可以是列表,数组,字符串,字典,集合。第四个delattr是删除对象的属性,如果对象的属性不存在会报错,存在就删除返回值是None。




我们看到上面的help第一个参数都是object说明只能有一个对象,因为是单数嘛。


我们称这种方法是用属性去修改属性,什么意思下面你们会知道的,但是它是有固定格式的。property第一个参数是得到这个东西的属性值的函数名,第二个是设定值的函数名,第三个是删除函数,上面其实已经给出了例子,我们来实际操作一下

我们通过访问a1.y可以得到,修改,删除a1.x。调用a1.y就相当于调用了函数a1.getx(),a1.y=3就是调用了a1.setx(3),del a1.y就是调用了a1.delx()。这是标准形式,我下面来说一些注意事项,

注意这个属性不能和它代表的属性名字重合,不然会报错。

这个形式是固定的,不能乱改,必须是上面的标准形式。你还可以同时修改两个属性,我下面用的是列表,也可以用元组,字典,集合,只要你赋值的时候赋值的value是对的就可以

一定要对应好

这种方法类似于封装,而且修改代码会很好改,而且对于用户来说操作是很简单的,用户不用去按照那些函数名才能得到,修改,删除属性值,那样需要三个函数,而现在就只需要对类的一个属性操作,而不是调用三个方法。

练习

这个其实不算很难的,注意方法里要写a.n而不是s.n





我们看到是有问题的,因为不知道为什么,这个stack属性的默认content还随着实例化对象改变,我认为这要么是一种bug要么是跟数据结构有关,但是如果默认属性是一个数值,我们看到它是不会随着实例化对象的属性值改变而改变,因为a2.c=1嘛,和上面的s1.content对比

那怎么办呢?我们需要修该代码,在每次实例化一个对象的时候,做一些操作


我们看到好像没有问题。但是看下面的一种情况

不知道为什么s1初始化的时候start没有起作用啊,因为=是深拷贝。我们换成浅拷贝就可以了



这一讲代码量比较少,就不给代码了,自己多打打代码还是好的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值