如果下面图片加载不出来,请到https://www.bilibili.com/read/cv273778,这都是我写的。
我就先来说一说学习对象和类的必要性,可能你不懂这些你也可以用python用得很开心。就像你去学开车,你不懂汽车的原理也可以学会,但是如果对于一个赛车手,懂得赛车的构造和原理还是对于你开车的速度和平稳性有很大的帮助。所以说要提高对自己的要求,不仅要知其然,还要知其所以然。
类和对象的定义
前面的十讲里面会是不是提到对象或者说这么一句话:python是面向对象的编程,可能你还会经常看见或者听见:python无处不对象。那么我们就先来看看什么叫做对象
![](https://i-blog.csdnimg.cn/blog_migrate/f380325ed251a8b18d9337a40b06e93a.png)
属性其实就是变量,还记得我们前面经常说的内置方法吗?其实就是对象里的函数,只是和一般的函数有不同,下面会说到。为了形象的说明什么是对象的属性和方法,我们来举现实中的例子,比如一个人作为对象,属性就是这个人身高,体重,性别,学历,等等,方法是什么呢?方法就是这个人能干什么,比如说我会编程,你给我一个产品要求,我用我编程这个能力可以出来一段满足要求的代码,我会控制,你给我几个控制指标,我可以用我控制这个能力
给你出来一个控制器。一个人就是一个对象,世界上有多少人呢?超过70亿。每一个人都有自己的属性和方法,那我们如果编一个代码来装这么多数据岂不是要累死。不必担心,我们还有类,对象和类的关系就是猫这个大类和你家里养的猫的关系,是工厂里的玩具模具和生产出的玩具或者说建筑图纸和根据图纸建造的每一间房子的关系。对象是具体的,类是抽象的,一个类对为它的对象给出了一个统一的定义,而这个类的对象则是符合这种定义的每一个实体。定义一个类的时候要尽可能的抽象,比如说我们定义人这么一个类,它的方法应该有哪些呢?这个人能干这些活,那个人能干这些活,这个人干的活可能那个人干不了,那个人的活这个人又可能干不了,应该依据谁来定义人这个类的方法呢?这时候,我们就不能选某个特定的人作为参照来定义方法,我们要虚拟出或者说抽象出一个完美的人,你能想到活他都能干,就按照这个抽象的人呢来定义方法,比如说他会编程,会数论,会数值分析,会泛函分析,会计算方法,会机器学习,会种地,会操作机床,会修电脑............你想到什么他就会干什么。说到这,可能有人就会问了既然python既然无处不对象,类难道不是对象吗?我们上面说的类指的都是类的定义,类定义结束以后,在python里,它就是一个类对象了。对这个类实例化之后的对象称为实例化对象(instance)。我们在python里面举个例子
![](https://i-blog.csdnimg.cn/blog_migrate/e49b508ba720ee855307fab48695fe79.png)
list关键字就代表列表这个类,a就是列表的一个实例化,同时我们 看到实例化不一定要出现工厂函数list,b也是一个实例,实例化以后我们可以调用sort方法和reverse方法对a进行操作,是没有问题的,但是我们看到直接list.reverse()是会报错的,说缺少一个参数,因为你没有实例化它,你没有给它列表参数,那你让python对谁排序?当然会报错,你只需要在括号里给它一个列表就可以正常运行了。
现实中的例子比如人类是一个类对象(人类已经被定义过了),笔者或者正在看这篇文章的你就是一个实例化对象,再比如猫是一个类对象吧,那么你家的猫,加菲猫,蓝胖子(机器猫),汤姆(我相信看我这篇文章的都应该看过汤姆和杰瑞吧),黑猫警长(这么你们没看过也应该知道吧,不然笔者又要感叹一波代沟了)都是实例化的猫。类可以有很多,范围可大可小,人是一个类,男人也是一个类,女人也是一个类,猫是一个类,白猫,黑猫都是一个类,类是可以很具体的,虽然一般不用也不推荐具体的类定义,上面也有说到类定义抽象一点比较好,泛化性好。这些类之间可能会有继承关系(继承下面也会讲到),前面都是在现实中说的,会容易把属性和方法对应到某一个东西,某一项技能。回到python里你可以随意创建自己想要的类,你想让这个类里有什么方法都可以,这些属性和方法会很抽象,不容易对应到什么东西后者什么技能,但是它们是可以对应到数据或者python里的操作的。我们在python里举个定义类并且实例化的例子。
类的定义和方法的调用以及属性的访问
首先要说明属性和方法不要重名,不然后写的覆盖原来的
![](https://i-blog.csdnimg.cn/blog_migrate/ae453ed0913928deb6d005d05a45cad4.png)
下面是定义一个矩形操作类,这个类的属性有长和宽,方法有得到长和宽,得到面积和得到周长三个。
![](https://i-blog.csdnimg.cn/blog_migrate/b3e144190bc5d3c116dc85676d4f1af7.png)
定义好了类以后,我们下面用r去实例化这个类对象,r就是一个实例对象。然后先后调用三个方法。
![](https://i-blog.csdnimg.cn/blog_migrate/bff8edc7fb6c8b45cf01425f0f905018.png)
输入长5,宽4,点ok
调用求面积和求周长方法。
![](https://i-blog.csdnimg.cn/blog_migrate/9f8692b543f938e7c3a7bdcec52bb375.png)
我们是可以直接访问类的属性的,得到的都是默认的值
![](https://i-blog.csdnimg.cn/blog_migrate/2d9e6d6570ac6f75c2a23c48924f9d31.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6871692dc31f82bace536dfbc95d636d.png)
从上图我们可以看到的是,r这个实例的属性length变成了5,而rectangle这个类的属性是没有随着实例或者说个例的属性变化而变化的,还是默认值1。直接调用rectangle这个类对象里面的某一个方法它报错了(当然这和你这个方法的定义是有关系的,如果我加一个p方法
![](https://i-blog.csdnimg.cn/blog_migrate/d9b748ebc7f6f78b0aef519dc4f02ecc.png)
![](https://i-blog.csdnimg.cn/blog_migrate/5a06171ee051f186e662b3c9af963903.png)
就是ok的。因为它的函数定义里面就不带参数,下面会讲这个self参数是什么作用)
这个self参数是很重要的,但是其实理解起来很简单。拿上面矩形操作类的代码为例,
![](https://i-blog.csdnimg.cn/blog_migrate/bff8edc7fb6c8b45cf01425f0f905018.png)
r=rectangle()实例化以后,r.getwl()这一句就调用了getwl里面的self就替换为r后面的方法也是一样,自动把self替换为r。还可以用下面的操作
![](https://i-blog.csdnimg.cn/blog_migrate/efd60de82142c49f37cfface0bb943f1.png)
q=rectangle(),这一句实例化是必须的,后面还可以用rectangle.getwl(q)和q.getwl()效果是一样的。而且rectangle.getwl(q)这种调用会让人更容易理解self,q这个实参的属性传递给了self。那么我们就可以把方法完全当作函数来看待,只需要记住q.getwl()和rectangle.getwl(q)是等价的。自然的我们就会想,其实self这个参数可以换成其它字母,但是要和下面的属性.length和.width前面的字母一致比如。
![](https://i-blog.csdnimg.cn/blog_migrate/1e94f3e7a420da255a95898bd0977166.png)
![](https://i-blog.csdnimg.cn/blog_migrate/669ad72dc36e1a6bcda0251bf8104870.png)
但是类定义里面的方法还是和函数有不同点的。
函数和方法的不同
方法是不能被其它同级缩进的方法引用的。
![](https://i-blog.csdnimg.cn/blog_migrate/1e3bf9494854eee38779b41589f4a649.png)
![](https://i-blog.csdnimg.cn/blog_migrate/ad48db1bf714d5749d8234876ee3d0c7.png)
显示p没有定义,我们现在不在类定义里操作一下:
![](https://i-blog.csdnimg.cn/blog_migrate/c91ae907a86e939d54aebea38b517a43.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b7b888b9668b3af288a82e3f3ddb65c6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/56c90f55a98a7ccf19dd7f977580064f.png)
注意我说的是同级缩进的方法,如果是嵌套的函数,即外部函数当然可以调用内部函数。
![](https://i-blog.csdnimg.cn/blog_migrate/86516bae74b6341618448680d81acf70.png)
而且只要不是方法层面的函数是可以和前面的函数一样调用别的函数的,
![](https://i-blog.csdnimg.cn/blog_migrate/cb9d90ff8c97c00d8769edfd7d547406.png)
![](https://i-blog.csdnimg.cn/blog_migrate/78f39a6c7f2ed7abc40b17fc753b586a.png)
调用类定义外面的函数很灵活,方法也可以调用,因为它在类定义外面嘛,不是方法。
![](https://i-blog.csdnimg.cn/blog_migrate/037aeb97a59bc3f677662d8afa30d218.png)
方法递归(自己调用自己)也不行,
![](https://i-blog.csdnimg.cn/blog_migrate/26396d1d411a3d6139f530cce63efeaa.png)
方法内的函数是可以递归的。
![](https://i-blog.csdnimg.cn/blog_migrate/d98c5aa532652eae3932de29b6404b6e.png)
最后总结一下方法和函数的不同,1调用方法有一种特殊的形式:实例.方法()。2一个方法里是不能调用方法的(包括自己)。如果是方法里嵌套定义的函数和普通的函数一样。下面介绍OO的特征
OO的特征
![](https://i-blog.csdnimg.cn/blog_migrate/5901b4d499755906d82d431e3037184f.png)
第一个特征
![](https://i-blog.csdnimg.cn/blog_migrate/3e5c559135c30ecc82b6a5051fa3e447.png)
封装我们很常见了,数据字符串层面的封装有列表,元组,字典,集合。代码的封装有函数,模块。这一讲的类是更高层面的封装,里面有属性有方法。封装除了调用方便,有时候可以减少重复性工作以外,还有隐蔽性,比如说还拿上面那个矩形操作类为例,你只知道里面有这个方法,方法的具体实现代码你是不知道的,这就像国外把高科技产品卖给我们,我们只知道该怎么用,里面的核心技术却不知道,因为国外有些国家对中国实行技术垄断,那么如果有些突发情况,比如说设备坏了,自己就不会修。并且还可以用名字重整来把一个变量变成私有变量(其实是伪私有,下面就会讲为什么是伪私有),
![](https://i-blog.csdnimg.cn/blog_migrate/7cafc08218c3a418a94e4a39f2ce681d.png)
但是其实我们还有办法访问得到,用下面的两种方法:
方法1,从内部通过方法访问到这个变量,通常我们把这个方法叫做访问器。
![](https://i-blog.csdnimg.cn/blog_migrate/060b4e3a51e77140bbcc2e65685e2c32.png)
上面的getname就是一个访问器,必须要实例化访问,看上面的报错就知道了。
方法2,比较暴力,python实际上是用下面这种格式给前面加双下划线的变量改了个下面的名字
![](https://i-blog.csdnimg.cn/blog_migrate/70e5616cfeb0ccf6603d15b665b125db.png)
![](https://i-blog.csdnimg.cn/blog_migrate/546dfd5f623df468f53c96759100dc03.png)
方法3,这个是我自己发现的,不调用方法,直接print出来
![](https://i-blog.csdnimg.cn/blog_migrate/75263361b6cc9976a6cabc21767ad93b.png)
第二个特征
![](https://i-blog.csdnimg.cn/blog_migrate/cdf62563d5e770aa81af4a4ece84d612.png)
![](https://i-blog.csdnimg.cn/blog_migrate/9e029997f8b34aec0c0c6e7910609f5e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e4b516456791fc66fda6dd1973b547af.png)
下面举个简单的例子
![](https://i-blog.csdnimg.cn/blog_migrate/081e7b5ced106bcebc58e9ce119dd6d6.png)
m这个类就是继承了list这个父类的子类,我们看到父类的方法子类也是可以用的。继承的一般语法
![](https://i-blog.csdnimg.cn/blog_migrate/0cd4b71613082792b702ca86477c83d8.png)
例子:
![](https://i-blog.csdnimg.cn/blog_migrate/05ae14b976df6dcc80d587ec6f3a95f9.png)
这里稍微提一下__init__这个魔法方法,被称为构造方法,更多的魔法方法这个系列后面后讲到的
![](https://i-blog.csdnimg.cn/blog_migrate/532f3fcd336c6298a95199d4e1a7aedf.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c3745366a995d83b752fd0e7ffe04a5c.png)
通过修改,我们增加了一个参数,那么在实例化时就必须要在括号内输入对应数量的参数,不然会报错。当然还可以设置默认参数
![](https://i-blog.csdnimg.cn/blog_migrate/97e96967ef182be08dd858163f692049.png)
还有一点__init__方法只能返回None
![](https://i-blog.csdnimg.cn/blog_migrate/d3b572b9fe431ba83928feccdeb40051.png)
![](https://i-blog.csdnimg.cn/blog_migrate/be55b39d73842fc62401ca297639f663.png)
![](https://i-blog.csdnimg.cn/blog_migrate/cf2ccb0940cb5dc33b8bdf015dc398be.png)
![](https://i-blog.csdnimg.cn/blog_migrate/99b30994bd65d5e22ed052749caeb8c7.png)
注意子类的属屏蔽性覆盖的是父类的属性和方法在子类里的拷贝,而没有改变父类的属性和方法。(想屏蔽父类的那个方法就只需要在子类里面重写方法,并且操作为pass即可)。
注意子类只会继承写在上面的父类,如果在继承之后修改父类,子类不会继承修改后的父类,而是保持不变,要想子类继承父类的修改,除非再写一遍修改代码,这就凸显了在py文件里写代码的好处,可以随时在原代码上修改。
![](https://i-blog.csdnimg.cn/blog_migrate/e45a5d2470972bf01547c2103f32e1ae.png)
还可以多重继承,语法为
![](https://i-blog.csdnimg.cn/blog_migrate/93fba952ca42e254c3afcb2c7efeefae.png)
并且多重继承是有优先级的,写在括号前面的优先级高,子类调用重名的属性和方法时会调用优先级高父类的属性和方法,
![](https://i-blog.csdnimg.cn/blog_migrate/a688a947b9c9285824384d212fa64b8f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/82b420493d350409fcd5d41f5addd7fa.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1f8f97168b18cfa02b80c968f1ca92c6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/85cd7173d9e0e52fad5d50afef88ffdc.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c5e48fdf570954fd528644868a2efa3d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a6a6120eb66c053cbfe9fee081ece537.png)
![](https://i-blog.csdnimg.cn/blog_migrate/f8140013352b31f90f0364ac61853094.png)
这里我稍微说一下多重继承代码怎么读,e=d(),e就是d的一个实例了,并且实例化过程会自动调用__init__方法,我们进入d的__init__方法,遇到括号,就把第一个参数换成e就ok,比如我们先进入b.__init__(e),然后进到b.__init__(e):e.n-=1(n属性继承自b,这里只写b是因为b写在前面,上面讲过的,你懂得,b继承自a,默认是2),后面相信大家也会读了。最后e.n=0,因为进入了a两次嘛。这里说一个概念,其实a.__init__(s)称之为未绑定的父类方法,因为它传进去的是子类的实例对象,也就是s=e。
![](https://i-blog.csdnimg.cn/blog_migrate/5547e23a7f964d907c55c09a015d92d3.png)
如何避免多重继承问题?我们可以用super函数,下面说的基类也就是父类
![](https://i-blog.csdnimg.cn/blog_migrate/c8e5620b4817900bbe4ee7ac2cc0fdb1.png)
实现
![](https://i-blog.csdnimg.cn/blog_migrate/f5a9d8b13e2c2b954bc334be819ff82f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b1589e1a5491a4647cd87d8e3b5fd8ca.png)
注意看这个结果,e.n=1,说明只进入a一次。更多多重继承讲解请看http://bbs.fishc.com/thread-48759-1-1.html
第三个特征
![](https://i-blog.csdnimg.cn/blog_migrate/a41c9ee8efe62e1dbe7a8d1dbb259669.png)
多态就是对不同的对象调用相同的方法,可能产生不同的结果。
![](https://i-blog.csdnimg.cn/blog_migrate/4cc62606bebedcc9d6c20cff32a4e68c.png)
总结一下三个特征就是
![](https://i-blog.csdnimg.cn/blog_migrate/f2c6cedbd3640066c1227d9383cd498b.png)
下面是笔者做的一些尝试
![](https://i-blog.csdnimg.cn/blog_migrate/66e431c2e3af63ccd2481452e6030557.png)
说明类定义里面也是局部变量,需要用类的属性来访问,还有就是方法里面出现类定义里面的同名参数n和以前学的函数里的局部变量一样,python会把同名的全局变量屏蔽掉,在堆栈里为局部变量n开辟空间,方法调用完成后,局部变量n就被弹出堆栈区了。
如果我们先调用geta或者先调用getl方法
![](https://i-blog.csdnimg.cn/blog_migrate/bfd4800e4d33588660cb5e6acf9716c6.png)
看到是按照默认参数计算的,如果不给默认参数呢?
![](https://i-blog.csdnimg.cn/blog_migrate/b7db2e2d9f2befdc19ca8d25232e1ff5.png)
说明默认参数还是很有必要的,以防止你忘记时,报错,提高用户体验。
类定义是可以嵌套的:
![](https://i-blog.csdnimg.cn/blog_migrate/db6326e4538c8df454510b3fd8f3ceea.png)
类的实例化必须在类定义之后
![](https://i-blog.csdnimg.cn/blog_migrate/a07597618db713484d1e0024f35bfb78.png)
另外我在这里再提醒一下不要乱用global,下面就是在类定义里乱用global带来的一些 错误
![](https://i-blog.csdnimg.cn/blog_migrate/cb39f89f1f75e31981d063a95b2b7ce3.png)
对比上面的两幅图,是不是很奇怪?我来告诉你为什么。先看下面的图
![](https://i-blog.csdnimg.cn/blog_migrate/3c75ded98d966738a905ed706ff064f9.png)
这说明前面b=0这一句其实在python里存的是a.b=0,b是按照a的一个属性来存的,而globa
b 是按照一个全局变量来存的,你看到它们的id都不一样。在类定义里定义的属性名可以和全局变量名重合的,因为在类定义外访问它们的方法不一样。一个是a.b一个是直接b。所以你应该可以理解上面为什么会报NameError了吧,不理解的话请看
![](https://i-blog.csdnimg.cn/blog_migrate/3b7c4e5d0812e52419b5973d6c87990c.png)
你在没有给全局变量b赋值时就调用了,当然报错。正确的做法是
![](https://i-blog.csdnimg.cn/blog_migrate/5c3e79790927880ce36ac9b28b105acf.png)
几个小练习
![](https://i-blog.csdnimg.cn/blog_migrate/7eb22217f158adfd32633a82521d5100.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a6f8834a1fd0edcbbd45e36f059e6547.png)
![](https://i-blog.csdnimg.cn/blog_migrate/dd1ad293539d3f774d426297bca4736b.png)
我上面是用了easygui并且把功能扩展为可以输入星期数和成人数小孩数,计算费用。
![](https://i-blog.csdnimg.cn/blog_migrate/ce5a3571a2852d6724de6924436bc780.png)
![](https://i-blog.csdnimg.cn/blog_migrate/67c537c74133101d7550863b5a55f954.png)
![](https://i-blog.csdnimg.cn/blog_migrate/0eccdfabe0d134d25aef045ce450645b.png)
我结合easygui扩展了功能,选了几个可以很快结束的参数。
![](https://i-blog.csdnimg.cn/blog_migrate/556b46b72c8f829a6ccaafb2b63b5575.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c8d633a83ef77f980a09cf397a0fed98.png)
![](https://i-blog.csdnimg.cn/blog_migrate/0f030e3909ca5d77def8c0eff463a87d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b019d93252745b9a0e104e7b048c95e1.png)
同样我们和上一讲的easygui结合一下
![](https://i-blog.csdnimg.cn/blog_migrate/37dd3ee3ee39eb7083eb4c4fcdc60f9a.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8e929b9f0ff99ab567f12c7417fe7f45.png)
![](https://i-blog.csdnimg.cn/blog_migrate/32f939c2db2a6f87f5b9a129c2b44b5f.png)
![](https://i-blog.csdnimg.cn/blog_migrate/fc02dc9a359a9598ea26ae3b6b9556d8.png)
![](https://i-blog.csdnimg.cn/blog_migrate/29024022bc1ac4fa69f248555fada840.png)
剩下的你们自己试好吧。
代码百度云链接链接:https://pan.baidu.com/s/1j-vN9KWZTSvwCMby7yU8aQ 密码:l3p9