Pythin语言的面向对象(上)
1. 面向对象简介
Python 是一门 面向对象 的 编程语言。
所谓 面向对象的语言,简单理解就是:语言中的 所有操作 都是通过 对象 来进行的。
这么解释呢 还是有点抽象,那我们来打几个 比方:你想做件事,你要先去找个 对象;你想要一个 数值,我先找一个 对象 来保存需要的 数值,之后再对 对象 进行调取;你想要一个 字符串,我先找一个 对象 来保存需要的 字符串,之后再对 对象 进行调取;你想要一个 函数,我要先有一个 函数对象 来保存需要的 函数,之后再对 对象 进行调取……回到最初,我们开篇讲的第一句话:Python一切皆对象。这就是对 面向对象的语言 的简单理解。
在我们进入 面向对象 的世界之前,我们先来对 计算机的编程思想 的发展简单的了解一下。
面向对象 的 编程思想 是由 面向过程 的 编程思想 在实际操作过程中一步一步的 更新 和 迭代 衍化过来的。那什么是 面向过程 呢?
面向过程:
面向过程 指将我们的程序 分解 为一个一个的 执行步骤,通过对 每个步骤 的 抽象 来完成程序;
举个例子:孩子想吃瓜了,怎么办?
1. 妈妈穿衣服穿鞋出门
2. 妈妈骑上电动车
3. 妈妈到超市门口放好电动车
4. 妈妈买西瓜
5. 妈妈 结账
6. 妈妈骑电动车回家
7. 到家孩子吃西瓜
案例看完了,其实整个的这些步骤如果要编写一成一个 程序,我们一般会按照 面向过程 的编写方法进行编写;从 妈妈穿衣服穿鞋出门 --> 到家孩子吃西瓜 这整个 过程 就是一个 面向过程 的编写思想;这整个事情的 目的 呢,我们需要 7步 来完成实现。我们把实现这个 目的 编写成了 第1步实现什么,第2步实现什么,第3步实现什么……把整个事情分成了一个步骤一个步骤,这就是一个 面向过程 的编写方式;整个这个过程,我们关注的不是“人”,不是“东西”……而是“事”。
回到我们现实生活,这整个的步骤和这种生活方式是没什么问题的;但是一旦到了我们 程序 中,我们写完了一段代码不可能就这么完了对吧;更多可能的还是我们还需要 复用 的。假如,这一段代码我们需要 复用 1000次,该怎么办呢?而且我们大家都知道:单纯的 复用 不是最麻烦的,真正麻烦的是程序后期的 更新、维护 和 迭代。现在是“妈妈买瓜”;今天妈妈不在家,怎么执行“爸爸买瓜”呢?再或者,外面现在在下暴雨,妈妈不能穿常规衣服去买瓜了,现在妈妈外面需要加套一件“雨衣”,这又该怎么操作呢?等等等等,这是不是每出现一次 需求微调,我们是不是就要 重新编写 代码;这不仅给我带来很多的不方便,更多的是对 代码的正常运行 进行 不断的挑战。
面向过程 这种编写方式往往 只适用 于 一个功能,如果要实现 别的功能 需重新编写新的程序,往往对代码的 复用性 比较 低。
这种编程方式 符合 人类的思维,编写起来 比较容易。
假设上面这段 代码 我一定要它实现别的功能,比如说 我一定要这段代码 重复1000次,或者说我就是要 “孩子吃肉” 该怎么办?出门左转,小编我是不是刚写完三篇我对 Python语言的函数 的理解;我们把上面这段 代码 放进 函数 里,如果我想 执行1000次 直接调用或者循环 函数 1000次就可以了;我只要改原函数的一行代码,这整个循环的1000次的代码都改了。
这样的操作没什么问题,而且 函数 的出现也是为了来解决类似的问题而存在的,而单纯 函数 的这个功能仅仅也只是增加的 函数复用性 这一个功能,而上面这个 函数 也只是在做一件事“孩子吃瓜”。如果我想要 “孩子吃肉”、“孩子吃蔬菜”、“孩子吃水果” 该怎么办?它们是不是有很多 相同 或者 相似 的地方;最起码,它整体的运行框架是一致的,只是一些小细节是不一样。讲到这,有些一直追更的读者可能会说:最近的一篇文章 10.Python语言的函数(下) 里刚讲过的 高级函数 和 Python语言的装饰器 来扩展函数的功能;通过 高级函数 来对 孩子 的一些行为进行进行管理;这么操作,虽然需求效果是达到了,但是 高级函数 和 Python语言的装饰器 是否又增加的程序的复杂性;所以,还是前面的那句话:这种编程方式 符合 人类的思维,编写起来 比较容易,但是 功能 比较单一,不利于代码的 复用性。
现在 面向过程 编程编写出来的代码的功能过於单一,用 函数 和 装饰器 进行修改增加的程序的复杂性。那我们还有别的方法吗?
还是用我们现实生活小事情来作为实例:如果读者朋友你现在是 一家之主,我们什么事都要 操心,指挥别人做这个做那个。你说:孩儿他妈,你出门别忘了穿鞋啊;孩子他妈,你买东西别忘了骑车;孩子他妈,你买了这个瓜别忘了结账……你是操碎了心,对吧?你这个孩他妈去买东西不能明抢对吧,我还是要结账的对吧。所以,你这样子在家肯定是天天要干仗的对吧。
现在我们就有一种新的思维方式,你想轻松怎么办?是不是你只需要告诉别人做什么就可以了。至于怎么个过程,别的就不管了;比如我现在就跟孩子他妈说 一句话:给孩子买个西瓜。是不是就O了,对吧。那至于你穿没穿鞋,开没开车,结没结账,我就不管了。最终孩子他妈把西瓜买回来了;然后,叭,往孩子面前一搁:孩儿,吃吧,你妈给你买的。这不是OK了。这种行为方式,在编程里我们称之为:面向对象 的编程语言。
刚刚我们通过 面向过程 完成整个操作用了 7个步骤,那 面向对象 是不是只是把这 7个步骤 变成 一个操作:孩子他妈给孩子买西瓜。完成。是不是就O了,很快速、简单、简洁。现在就是用 一行代码 替换了 七行代码 。作为一个 立志 成为 优秀计算机工程师 的你,会选择用那种方法?毫无疑问,一定是 面向对象 的编程方法了。
那我们再来思考思考:这个 面向对象 我们面对的是谁?或者说这个 对象 是谁?函数?我?孩子?孩子他妈?西瓜?吃西瓜?再次回到我们的 起点:Python一切皆对象。其实对于 面向对象 的编程语言来说 一切都是对象。
举几个例子:“穿鞋” 这个动作是不是就是一个 对象;“电动车” 这个动作是不是就是一个 对象;“骑” 这个动作是不是就是一个 对象;包括“结账” 这个动作是不是也是一个 对象……那么大家会说:这么多的 对象 有什么好处?我们又怎么把一系列的数据或者行为保存到一个个的 对象 当中。
比如说:就给 孩子吃瓜 这个事情;我就可以把这个事情保存到 妈妈 这个 对象 当中,至于她怎么去买,都是由这个 对象内部定义的信息决定 的。对象是什么?对象 就是一个 存储数据的容器;它的好处是什么呀?好处就是:我想让孩子吃瓜,我就让孩子他妈去买瓜;我想让孩子吃菜,我就让孩子他妈去买菜……真正的细节就在 对象 里,我不需要再操心了。什么 穿鞋、开电动车、结账……只要我们把 对象 写好了、完善了,是不是就不需要再担心了。
再比如说大家应该用过这个爬虫的一个模块:requests;requests.get(‘http://xxxxxxxxxxxx’)。这个 get(’ ') 里面需要传递一个URL地址;此时你愿意传 百度 你就传 百度,你愿意传 新浪 就传一个 新浪,你愿意传一个 小说网站 你就传一个 小说网站……具体细节我不管,我只是把这个对象给你定义好了,你直接用是不是就OK了。其实 这些操作 都是什么?其实,这些操作就是 面向对象 操作。
那个此时大家心里可能有一个 疑问:这个叫 孩子吃瓜 这件事,里面 妈妈 这个对象中不也得是这么一步一步实现这个步骤吗?这个呢确实没错,但是 区别 在于什么呀?区别在于 是不是这些 步骤 已经存在 于妈妈这个 对象当中 了。我们只要写好这个 对象 是不是OK了,对吧?所以呢,我们来小小的总结一下:
1) 面向对象 的编程语言,关注 的是 对象,而 不注重 过程,对于 面向对象来说:一切皆对象;
2) 面向对象 的 编程思想,将 所有功能 统一保存 到 对应 的 对象 中,要使用某个功能,直接找到 对应的对象 即可;
3) 这种 编码方式 比较 容易阅读,并且 易于维护,容易复用。但是编写的过程中 不太符合 常规的思维方式,刚开始编写的时候相对会比较麻烦。
2. 类(class)
我们目前学习的 对象 都是 Python的内置对象,但是 内置对象 并不都能 满足我们的需求,所以我们在开发的过程中经常要 自定义一些对象。
类 简单理解:它就是相当于一个 图纸。这个图纸是干嘛呀?我们以前说 对象 相当于是 一个容器,那么你现在要 创建一个对象,或者你要自己 定义这么一个对象。前面我们说:对象 就像是 容器,那这个这个 容器 是 方 的还是 圆 的?这个容器里面是有 一个格、两个格 还是 N个格……那这 属性 些都是 由谁决定 的呢?由谁?是不是就是由我们说的这个 图纸 来决定的,对吧。那么你要创建这个 容器,那就得先选择 图纸;这个 图纸 就是我们所说的这个 类 ;所以 类 简单的理解 就是一张 图纸,在程序中我们需要根据 类 来 创建对象;类 就是对象的 图纸。
如果上面的这些介绍还没有让你完全明白,那我们来 举个例子:现在呢,我们手上有张 图纸,现在假设,这是一张可以造汽车的图纸;这个时候,我们是不是可以根据这张 图纸,我们造一辆 红色汽车、一辆 蓝色汽车、一辆 黑色汽车……对吧,那么这些车是不是就是 对象,红色汽车 一个 对象、蓝色汽车 一个 对象、黑色汽车 一个 对象、等等等等;那这些对象是从哪来的?是不是根据 图纸 造出来的。所以说我们可以这么理解:我们又称 对象 是 类 的 实例(instance)。“ 对象 是 类 的 实例 ”这句话我们又怎么理解?我们说这张 图纸 它只是一张 纸,它没有什么 实际的形态,但是我们可以 根据原图纸来造出一个车,是不是就相当于把这张 图纸 给他 实例 出来,就是有一个现实中大家可以看的到的东西。
所以,我们也称:对象 是 类 的 实例(instance)。如果 多个对象 是通过 一个类 创建的,我们称 这些对象 是:一类对象。这 一类对象 我们可以用前面的“造汽车”来理解:虽然 车 的 颜色 不一样,但 车 的 型号 全部都是一样的。再比如说你这个对象是通过“人”这个对象创建的,那就是一个 “人类”;假如这个对象是通过“狗”这个对象创建的,那就是一个 “狗类”……到此呢,类的介绍就已经差不多了;那我们现在再一起回顾一下:我们到现在都学过那些类?int() 、string() 、float() 、bool() 、list() ……那么这些这些都是什么?这些都是 类。为什么这么说?来看 参考实例:
a = 1
print(a) # 1
a = int(1)
print(a) # 1
实例中,第一个a
是什么?是不是一个整数;两个print()
的结果都是一样的吧,都是“整数 1”;只是这第二种方法我们平时写的比较少,第二个a
的意思是不是:接收数据强制类型转换为整形。
那我们再来看一下:int() 、string() 、float() 、bool() 、list() ……这些Python语言内置的 类 有什么特点?是不是他们都是 小写字母开头,(括号)结尾 的。所以,当我们需要 自定义类 的时候需要以 大写字母 来开头。当然,这是下一块内容,我们等下再说。
回到前面,既然我们刚刚说:int()
是是一个类;那么a = int(1)
这行代码就今天就可以完整的理解:它是创建了一个int()
的类的实例。那这个int()
的类的实例我们怎么理解?来看下面的 参考实例:
def fn_a():
pass
a = fn_a
print(a,type(a)) # <function fn_a at 0x0000028125B6D160> <class 'function'>
a = int(1)
print(a,type(a)) # 1 <class 'int'>
a = str('Python')
print(a,type(a)) # Python <class 'str'>
通过观察 参考实例 的运行结果我们可以逐渐清晰:通过 type() 函数 返回对象的类型我们发现 a = int(1)
的对象类型是 class(类),class(类) 的属性是 'int'
;而 a = fn_a
的对象类型是 function(函数)。虽然它们都很像,但是它们 不是一个东西。同样的 a = str('Python')
的对象类型也是 class(类),class(类) 的属性是 'str'
。
说这么多,那读者朋友们一定会问了:我们怎么自己来定义一个 类 啊?其实,我们自定义一个 类 就非常简单了;我们自己定义一个类就使用Python语言 内置的关键字 class。
语法实例:
# 创建 类 的语法:
class Class_name([父类]):
pass
我们本篇文章暂时不讲类 () 里的内容(下一篇),我们先讲一个老生常谈的话题:规范。在这里,我们说当然也就是 类的命名规范。自定义的 类, 类 的名字要使用大写字母开头;常规的 类 的命名方法我们一般使用 大驼峰命名法
自定义 类 的 语法 以及 相关的注意事项 都已经讲过了,那我们现在就一起来创建我们的第一个类吧。
class MyClass():
pass
print(MyClass) # <class '__main__.MyClass'>
我们已经成功的自己创建了第一个 类,并且通过print(MyClass)
打印出的结果得到:<class '__main__.MyClass'>
得到的结果是什么意思呢?当前打印的结果是一个 类,__main__
表示当前的 类文件 是 主文件,运行在 主文件 里这个 类 的名字叫MyClass
。
前面说过:类 就是 对象 的 图纸。我们现在已经成功的完成一个 类 的创建了,那我们怎么来创建一个 对象 呢?其实呢,这个操作也是非常的简单。来看 参考实例:
class MyClass():
pass
mc = MyClass()
这个操作是什么意思呢:我使用MyClass
这个 类 创建了一个对象,这个对象叫做 mc
;mc
就是通过 MyClass
这个 类 创建的 对象;mc
就是通过 MyClass
这个 类 的 实例(instance)。我们来通过 函数print()
和 函数type()
一起来看看 mc
打印出来的结果。
参考实例:
class MyClass():
pass
mc = MyClass()
print(mc,type(mc)) # <__main__.MyClass object at 0x000002698C928400> <class '__main__.MyClass'>
跳过解释操作,直接翻译结果的意思:首先解释 print(mc)
出来的结果 <__main__.MyClass object at 0x000002698C928400>
;我们当前所在的 运行文件 是 主文件,打印的 变量mc
是 类 MyClass
实例化 后的 对象,存储于 0x000002698C928400
这个内存位置当中。type(mc)
打印的是 变量mc
的类型,意思是 变量mc
是通过MyClass
这个 类 创建的 对象。
现在我们已经知道 如何创建一个 对象 已经 对象 打印出来的结果,那么如果一个 类 被多个 对象 实例化 会怎么样呢?
参考实例:
class MyClass():
pass
mc_1 = MyClass()
mc_2 = MyClass()
mc_3 = MyClass()
mc_4 = MyClass()
看上面的 参考实例 我们会发现 4个对象 mc_1
、mc_2
、mc_3
、mc_4
其实都属于一个 类 MyClass()
的实例,它们都是 一类对象;当然了,参考实例 毕竟相对的还是比较简单,我们一眼就能看出来。不过,如果以后我们在真实的开发场景中如何证明 mc_1
、mc_2
、mc_3
、mc_4
4个对象都属于 一类对象 呢?此时,我们来介绍 Python语言 中第一个对 类 进行操作的 函数 isinstance()
。
参考实例:
class MyClas():
pass
class MyClass():
pass
mc_1 = MyClass()
mc_2 = MyClass()
mc_3 = MyClass()
mc_4 = MyClass()
mc_5 = MyClas()
q = isinstance(mc_1,MyClass)
w = isinstance(mc_2,MyClass)
e = isinstance(mc_3,MyClass)
r = isinstance(mc_4,MyClass)
t = isinstance(mc_5,MyClass)
y = isinstance(int,MyClass)
print(q,w,e,r,t,y) # True True True True False False
通过观察上面的 参考实例 我们会发现:如果 函数 isinstance()
返回的是 True
则证明 对象 mc_n
是 类 MyClas()
的 实例化对象;如果返回 False
则证明 对象 mc_n
不是 类 MyClas()
的 实例化对象。
到此,我们先小结一下:
类 简单理解:它就是相当于一个 图纸,在编写程序前 汇总 我们的 各种需要,再根据 类 来 创建对象。
所以说:类 就是 对象 的 图纸。
我们也称:对象 是 类 的 实例(instance)。
# 创建 类 的语法:
class 类名([父类]):
pass
实例化类 / 引用 类 创建 对象 的方法:
class MyClass():
pass
mc = MyClass()
如果 多个对象 是通过 一个类 创建的,我们称 这些对象 是:一类对象。
最后是 验证函数 isinstance()
,验证 实例化对象 是否是由 类MyClas()
实例化的对象:
class MyClas():
pass
class MyClass():
pass
mc_1 = MyClass()
q = isinstance(mc_1,MyClass)
print(q) # True / False
3. 对象的创建流程
类 和 对象 的简介大致的我们都已经说完了,接下来呢,我们一起来说说 对象的创建流程。
看到这个内容,有些朋友就会问了:前面在讲 类 的时候不是已经说过 对象 是 类 的 实例(instance)。这还有什么 创建流程 的吗?还真有。接下来呢,我们就一起的来唠唠。
前面我们说了 类 就是 对象 的 图纸;我们可以根据这个 图纸 重复的 创建N个对象。你 创造 一台大众不够,我还要 创造 一台奥迪、 创造 一台宝马……我们 创造 了这么多的东西,举了这么多的案例,我们现在再回头看看:我们今天一直说的这个 类 到底是干嘛的?
其实 类 也是 一个 对象,类 就是 用来 创建对象 的 对象。(这句话很重要!!!)
换个理解说法:类 这个 对象 的 主要功能 或者说 唯一功能 就是:创建对象。
上面我们讲过:一个 对象 里要有 id、type 和 value。
举个例子 现在有一个 对象 a = 1
,这个 对象 在我们的 计算机内存 里 开辟 了一个 空间,在这个 新开辟的 内存空间 里除了存储 对象a
,还有 对象 在 新开辟的 内存空间 里的 地址id
,对象 本身的 类型type
,以及 对象 对应的 值value
。
那么我们说即然一个 对象 里要有 id、type 和 value;那么一个 类 里是不是也要有 id、type 和 value 这三个值啊?一起来看一下:
class MyClass():
pass
print(id(MyClass),type(MyClass)) # 2855264727088 <class 'type'>
看上面实例的返回值:第一个2855264727088
,我想读者朋友你应该没什么问题,这就是这个 对象 在 计算机内存里新开辟的 内存空间 的 地址id
,这个还是很简洁明了,很好理解的;可是后面返回的结果<class 'type'>
就有些不明白了,MyClass
它的 类型 是 'type'
?这个怎么理解?这返回值的意思是不是说明了:类 是'type'
类型的一个对象;定义 类 实际上是定义了一个'type'
类型的 对象。
看到这,大家可能已经有点懵了。这绕来绕去怎么像是 绕口令 似的~其实也没那么的复杂,我来解释一下这在 “定义一个 类” 的过程中究竟都发生了哪些事。
参考实例:
class MyClass():
pass
mc = MyClass()
通过拆解上面的 参考实例 我们得到以下的 思维导图:
通过 导图 我们发现:我们首先呢创建了一张 图纸 叫做MyClass
,这是我们 自定义类,我们把它呢存放在一个 变量 里,寄存的变量 你叫它MyClass
也行,叫它ABC
都可以; 变量MyClass
的内存地址值为0x123
,前面说过:只要是 对象,就一定会在 计算机的内存 里开辟一个专属于某一变量的 内存空间。那么 变量MyClass
的 内存空间 里是否就有 id:0x123
、type:class 'type'
(只不过 自定义类MyClass
的 类型 有点 特殊) 和 value(现在暂时不讲它的内容) 这几个基础信息。
接下来呢,我们又写了另外的一行代码:mc = MyClass()
;通过 变量mc
实例化了我们 自定义 的这个 对象 MyClass()
形成的一个 新的对象,既然是一个 新的对象 那也就有它自己专属的 内存空间,既 id:0x223
。那么 变量mc
的 内存空间 里是否有 id:0x223
、type:MyClass
和 value(现在暂时不讲它的内容) 这几个基础信息;只不过呢,此时我们实例化的对象mc
它的属性 type 是 MyClass
这个 类对象,而不是 自定义类 MyClass
的 类型class 'type'
。
到此呢,我们算是把 类 以及它是如何 实例化,还有 实例化 后的 对象 和 原本的 类 到底有什么区别 算是解释清楚了。那么我们接着往下看。
现在我们发现通过这个MyClass
这个 自定义类 创建的 对象,但它三个 基础属性 里的 value
是不是 没有值 啊?是不是?也就是说:这个 对象 的里面实际上是 什么都没有 的对吧,就相当于是一个 空的盒子。那么,此时,就会有读者疑问了,你们一定会问:小编,这个 空的盒子 它有什么用啊?那么此时,小编我就也要问你一句了:既然是 空的盒子,那我们是不是就可以往里面 添加东西 了?😃
那这个 添加东西 要怎么 添加 呢?我们先来讲解语法:
首先呢,我们要先定义 添加东西 的操作的意义:我们可以向 对象 中 添加变量,对象 中 添加的变量 我们称之为 属性。 语法:对象.属性名 = 属性值
只看这些抽象的文字,大家肯定也是看不懂,接下来跟着小编我的 参考实例 一起来理解,上面这段话究竟是什么意思。
参考实例:
回到前面我们创建的 自定义类对象MyClass
和 实例化对象mc
class MyClass:
pass
mc =MyClass()
此时,我们添加一行代码mc.name = '蜘蛛侠'
,即:
class MyClass:
pass
mc = MyClass()
mc.name = '蜘蛛侠'
运行后我们会发现:最后什么都没输出。什么都没有?不过没关系,问题等等说,对于 励志未来做一个 优秀程序员 的我们来说:我们当下编辑的代码,只要 没报错 就是最大的 进步。在此,给我们彼此一个掌声~ 😃 闲言少叙,那我们来自问自答一句:我们添加的代码mc.name = '蜘蛛侠'
是在做无用功吗?肯定不是的了。来看 参考实例:
class MyClass:
pass
mc = MyClass()
mc.name = '蜘蛛侠'
print(mc.name) # 蜘蛛侠
通过参考实例我们发现:代码mc.name = '蜘蛛侠'
的这个操作的意思是 我们向 实例化对象mc
的 value 里添加了一个对象 name = '蜘蛛侠'
。
至于如何查看我们的 添加 是否成功,我们只需要通过print( )
打印以下这个新添加的 对象 就可以确定了。
现在,我们已经知道如何的向 实例化对象 的 value 里添加 对象属性,也知道如何的 验证 我们向 value 里添加的 对象属性 是否完成。那么又有一个问来了:现在我们的 自定义类对象MyClass
实例化 了两个 实例化对象,分别是 实例化对象mc
和 实例化对象mc_1
,通过上面的 案例 已知我们向 实例化对象mc
里添加了一个属性mc.name = '蜘蛛侠'
,不过,此时我们如果打印 实例化对象mc_1
的 value 里的name
属性,最后会是什么结果呢?即 print(mc_1.name)
。看以下 参考实例:
class MyClass:
pass
mc = MyClass()
mc_1 = MyClass()
mc.name = '蜘蛛侠'
print(mc_1.name) # AttributeError: 'MyClass' object has no attribute 'name'
AttributeError: 'MyClass' object has no attribute 'name'
翻译一下:属性错误:对象'MyClass'
里不存在名叫'name'
的属性。那么,我们来解释一下,系统为什么会反馈回这个错误:此时在内存空间中多出了一个名为mc_1
的变量,此时我们也假设这个变量开辟的 内存空间 的地址值为0x323
。此时,我们发现:新的 实例化对象mc_1
只是实例化了我们的 自定义类对象MyClass
;而代码mc.name = '蜘蛛侠'
的这个操作的意思是向 实例化对象mc
里添加一个 name = '蜘蛛侠'
的 属性,此操作和 自定义类对象MyClass
没任何关系。所以,我们打印 实例化对象mc_1
的 value 里的name
属性,自然也就会反馈 属性错误 这个提示了。
此时,如果有读者朋友们说:我就是想要向 实例化对象mc_1
添加一个属性怎么办呢?自然是:想添加就添加咯。再看 参考实例:
class MyClass:
pass
mc = MyClass()
mc.name = '蜘蛛侠'
mc_1 = MyClass()
mc_1.name = '钢铁侠'
print(mc.name) # 蜘蛛侠
print(mc_1.name) # 钢铁侠
到此,我们先小结一下:
前面我们小结过了 实例化类 / 引用 类 创建 对象 的方法:
class MyClass():
pass
mc = MyClass()
这一块呢,我们主要讲了:1) 什么是实例化对象;2) 整个自定义对象实例化的创建流程;以及呢 3) 如何向实例化对象中添加属性 value 的 值:
class MyClass():
pass
mc = MyClass()
mc.name = '蜘蛛侠'
print(mc.name) # 蜘蛛侠
4. 类的定义
自古:没有规矩,不成方圆。曾几何时,我最好的朋友 L先生 和我讲过一段话,让我对 整个计算机的认识 产生了 醍醐灌顶 的 升维,也让我在后来自学编程的时候有 降维打击 的畅通感;那他和我说了什么呢?
L先生 问我:你认为 计算机 或者说 编程 它 真正的核心 是什么?是 语法?是 语言?是 算法?是 数学?虽然它们都有或多或少的关系,但是 也只是 有点关系。计算机 真正的核心是 管理。你没有听错,就是 管理。管理 这个词在 普通人 的理解里就是:上级管理下级,让他们好好干活,别出幺蛾子、别犯错;管理 这个词在 管理高手 的理解里就是:场控;了解管理 市场、了解管理 客户、了解管理 产品、了解管理 公司、了解管理 行政、了解管理 供需、了解管理 员工……通过 不同的管理方法 管理着 各个对象 的 平衡,让他们彼此之间 和谐良性 的 交互运转,彼此 共同的平稳增长,直至达成我们最终的目标。你看,这和 计算机原理 是不是 一模一样 的。了解管理 每个代码、了解管理 语言语法、了解管理 前端后端、了解管理 不同框架、了解管理 交互运行、了解管理 环境变化、了解管理 访问运行、了解管理 数据变化……通过对 不同模块运转 的 管理 使 各个对象 运转达到 一定的平衡,让他们彼此之间 和谐良性 的 交互运转,彼此 共同的平稳增长,直至达成我们最终的目标。再把各个 限制、条件、规则、原理、因果 再 细分、拆解、拼装,你看“计算机”真正的核心法则是不是就这些。
在讲接下来的一块前我没什么要用这么大的一段这么隆重的开场白呢?因为我们接下来要讲“计算机”真正的核心法则之一: Python语言 的 类的定义。
OK,闲言少叙,即然我们要开始讲 Python语言 的 类的定义,那么就把我们先前的 自定义类MyClass
拿过来吧。
参考实例:
class MyClass:
pass
mc = MyClass()
mc.name = '蜘蛛侠'
mc_1 = MyClass()
mc_1.name = '钢铁侠'
print(mc.name) # 蜘蛛侠
print(mc_1.name) # 钢铁侠
实例 看完了,结果 呢也知道了,此时我们是不是该问一问:一顿操作看着虎,你这操作是在干什么呀? 或者说:我们添加的这个name
有什么用?
其实,我们这么添加的name
没什么用,而且这个 操作 也 不太对。为什么 不太对?这最后的结果不是并没有 报错 吗。
我们现在呢来详细的看一看:首先,我们先来看看这个 MyClass()
,这个MyClass()
是什么,是不是我们 自定的义类 啊;我们前面说过这个 MyClass()
相当于是一个 盒子,虽然这个 盒子 里现在啥都没有,现在通过 MyClass()
创建的 对象 也是一个 空对象;但是呢,我们也不要小瞧了这个 空盒子,因为它是 万能 的。哪它是怎么个 万能 法呢?既然是个 空盒子 当然就是你想放什么就放什么;比如:name、age、title……等等等等,但是呢 代码mc.name = '蜘蛛侠'
和 代码mc_1.name = '钢铁侠'
这两个操作都是向 对象 里添加了 一个属性;我们是不是可以往这个容器里多放一些。
前面我们说过:类这个容器 相当于一个 图纸,那么既然是 图纸 那是不是就要有它的 规矩 啊。既不能 什么都不放,也不能 肆意的添加 属性 和 元素。
既然,我们说 图纸 要有它的 规矩,那么 我们接下来就来讲一个 概念:类 和 对象 都是对 现实生活中事物 的 抽象。
怎么 理解 这个 概念,或者说 我们要怎么 抽象 呢?举个例子 :假设,我现在想对一个人 抽象,那我们该怎么 抽象 呢?或者说 我们该怎么通过 一个类 或者 一个对象 来 描述 一个人。所谓的 抽象 不是说把什么东西 拓印 出来一个,而是把 某一事物整个 的 特点 和 属性 放到 程序里的一个容器 当中。
所以,我们先来简单的总结一下:所有实际的事物都是由 两部分 组成:
1. 数据(属性);
2. 行为(方法);
我们先来说这第一部分 数据(属性):人 有 年龄、身高、体重、三维、多少骨头、几个牙、几个眼睛、几个嘴巴、几处受伤……这些是不是都是 一个人 的 数据;一个人 除了 数据 还有什么?人 是不是还 可以跑、可以打架、可以骂人 等等,你可以做 很多很多 好的事情,也可以做 很多很多 不好的事情;这些是什么?是不是都是 一个人 的 行为(方法)。
和大家磨叽了这么多,接下来给大家整个案例来换换脑子:
参考实例: 尝试定义一个人类
class Person():
pass
p_1 = Person()
p_2 = Person()
我们 自定义 了一个 类Person
,并 实例化 了两个对象,分别是 p_1
和 p_2
。这两个 人 在 程序 里呈现的形式在这里我们就不再重复了。
这里呢,我给大家画了一个简单的 思维导图。如果 不太清楚 这两个 对象 在 程序 里呈现的形式,回到上面的 3. 对象的创建流程,把这两个 对象 代入到我为你画的 思维导图 里再仔细的思考一下,在这我们就不浪费大家的阅读空间了。
前面我们说过,现在的两个 实例化对象 p_1
和 p_2
是两个 空对象,或者说是两个 空盒子,两个 对象 里面现在啥东西都没有;虽然我们前面介绍过 可以通过 实例类名.属性 = 'xxxxx'
向 实例对象 里添加 属性,但是没啥用。为什么没啥用? 因为我们对 自定义 的这个 类 没什么约束,自然了 我们向这个 类 里添加的 属性 自然的也就也没什么意义。创建一个 对象,这个 对象 里如果什么 属性 都可以添加的话,那 创建的这个 对象 也就凉了,它就没什么意义了。此时,我们是不是就要问一句:怎么办? 我们 怎么做 才能让我们 创建的这个 对象 有意义呢?
在我们自己 自定义类 的时候是不是说过:类 里面有个东西,它叫做 代码块。此时,我们 自定义的类Person()
里代替了 代码块 的是不是 代码pass
啊。接下来我们就要再说一个 类的定义 的 概念 了,那就是:在 类 的 代码块 中,我们可以 定义 变量 和 函数;在 类 中我们定义的 变量 将会成为 所有实例对象 的 公共属性,所有实例对象 都可以访问这些 属性。
参考实例: 向 自定义类 Person
里添加两个属性,分别是 a = 10
和 b = 20
。
class Person():
a = 10 # 向 自定义类Person() 里添加一个属性 a = 10
b = 20 # 向 自定义类Person() 里添加一个属性 b = 20
p_1 = Person()
p_2 = Person()
按照我们刚刚说的 类的定义 的 概念,通过 自定义类Person
实例化 的两个对象 p_1
和 p_2
是不是就可以访问 类 里的两个属性了?闲言少叙,直接来 打印一下:
class Person():
a = 10 # 向 自定义类Person() 里添加一个属性 a = 10
b = 20 # 向 自定义类Person() 里添加一个属性 b = 20
p_1 = Person()
p_2 = Person()
print(p_1.a, p_2.b) # 10 20
通过最后 打印出来的结果 我们 验证 了 在 类 的 代码块 中,我们可以 定义 变量 和 函数;在 类 中我们定义的 变量 将会成为 所有实例对象 的 公共属性,所有实例对象 都可以访问这些 属性 这个定义。
那么我们接着往下看:前面说了,我们这个 自定义类Person
是一个 人类,但是我们刚刚定义的这两个变量 a = 10
和 b = 20
在实际的 Person()
操作中是不是并没有什么 意义 啊?在本块内容的开始时我们说过:我们 自定义类Person
要有 人类 共同的 特点 或者是 特征。所以,我们在 自定义类 的 代码块 里要尽可能多的定义一些 有意义 的 属性 和 方法。那什么样的 属性 和 方法 才是 有意义 的 属性 和 方法 呢?再简化一下问题:在我们 自定义类Person
定义哪些 属性 和 方法 才是 有意义 的 属性 和 方法 呢?一个 人,他是最起码的是不是要有一个 名字 啊。
参考实例: 向 自定义类 Person
里添加一个 name
属性。
class Person():
name = '钢铁侠'
p_1 = Person()
print(p_1.name) # 钢铁侠
虽然我们最后 打印的返回值 和上一个 参考实例 最后 打印的返回 的结果差不多,但是,你有没有发现,我们通过这种方式最后 打印出来的结果 是不是比 p_1.name = '钢铁侠'
这种通过 实例化类p_1
传输进去的属性信息要 方便、安全 而且还 好用 很多。
这里呢,我给大家画了一个简单的 思维导图。以画图的方式向大家 简明扼要 的表达一下 参考实例 的小程序在整个的计算机内存中的 演变过程。
回到我们 最初 的总结:所有实际的事物都是由 两部分 组成:
1. 数据(属性);
2. 行为(方法);
刚刚我们介绍完了 自定义类Person
的第一部分 数据(属性) 的 创建 和 访问,接下来我们再来讲讲 自定义类Person
的第二部分 行为(方法) 的 创建 和 访问。
我们说了 人 除了有 年龄、身高、体重、三维、多少骨头、几个牙、几个眼睛、几个嘴巴、几处受伤……这些,一个 人 是不是还 可以跑、可以打架、可以骂人 等等,你可以做 很多很多 好的事情,也可以做 很多很多 不好的事情;那 一个人 的这些 行为(方法) 我们又该怎么 创建 和 访问 呢。
闲言少叙,我们接下往下看 参考实例:
class Person():
a = 10 # 向 自定义类Person() 里添加一个属性 a = 10
b = 20 # 向 自定义类Person() 里添加一个属性 b = 20
name = '钢铁侠' # 向 自定义类Person() 里添加一个属性 name = '钢铁侠'
def speak(): # 向 自定义类Person() 里添加一个方法 speak()
print('Hi~')
p_1 = Person()
p_2 = Person()
print(p_1.name) # 钢铁侠
p_2.speak() # TypeError: speak() takes 0 positional arguments but 1 was given
咦,怎么报错了?我们先不看代码,我们先来看看这个 错误提示 TypeError: speak() takes 0 positional arguments but 1 was given
是什么意思。我们把这个 错误提示 翻译成中文,大致意思就是:对象错误:调用函数speak
时没有在 () 里传递进 位置参数 但是 在调用函数时最少要传递一个 位置参数。用大白话讲就是:你在 调用 我的时候什么信息都 没传,你要给我 传递一个参数。
在我们前面学习 函数 的时候知道:当我们 创建的函数后面 定义几个 形参,我们 调用 的时候就要传递几个 实参;如果我们在 定义函数 的时候没有定义 形参 的话,那我们在 调用该函数 的时候是不是就不用 再传递 进什么 实参 了。但此时我们调用 自定义类Person
里面的 方法函数 时,自定义类Person
里面的 方法函数 里明明没有定义 形参,可是 系统 现在需要我们传递进一个 实参,不然就给 报错提示;这不就 矛盾 了吗。
为什么会出现这个问题呢,等会呢我们在 5. 参数 self 里再详细的讲解,现在我们先尝试着向 自定义类Person
里创建 方法函数speak()
里添加一个 参数,然后我们再试着调用一下 p_2.speak(a)
,看下这最后的打印结果:
class Person():
a = 10 # 向 自定义类Person() 里添加一个属性 a = 10
b = 20 # 向 自定义类Person() 里添加一个属性 b = 20
name = '钢铁侠' # 向 自定义类Person() 里添加一个属性 name = '钢铁侠'
def speak(a): # 向 自定义类Person() 里添加一个方法 speak()
print('Hi~')
p_1 = Person()
p_2 = Person()
print(p_1.name) # 钢铁侠
p_2.speak(a) # NameError: name 'a' is not defined
咦,怎么还报错?我 最爱的读者朋友,你先消消气,先稳定一下 情绪。我们还是先不看代码,我们先来看看这个 错误提示 NameError: name 'a' is not defined
是什么意思。我们把这个 错误提示 翻译成中文,大致意思就是:名称错误:内有找到名字叫 'a'
的对象。
没有 实参 的调用没有定义 形参 的 自定义类Person
里创建 方法函数speak()
结果报错;添加了 实参 调用定义了 形参a
的 自定义类Person
里创建 方法函数speak(a)
结果也是报错;这,这,这……这在正常情况,我们都是要心态爆炸的了。但是,编程就是个 手艺 + 经验 的活,首先我们要保持自身的心态平衡。一次、两次的尝试 都还 出错,这只能证明 我们尝试的次数还是不够。废话不多说,我们现在就开始下一种方法的尝试。
参考实例:
class Person():
a = 10 # 向 自定义类Person() 里添加一个属性 a = 10
b = 20 # 向 自定义类Person() 里添加一个属性 b = 20
name = '钢铁侠' # 向 自定义类Person() 里添加一个属性 name = '钢铁侠'
def speak(): # 向 自定义类Person() 里添加一个方法 speak()
print('Hi~')
p_1 = Person()
p_2 = Person()
print(p_1.name) # 钢铁侠
p_2.speak(a) # NameError: name 'a' is not defined
还是 报了 NameError
错误;没事,两个 () 最多最多我们也就尝试 四次 就能得到所有结果了。
class Person():
a = 10 # 向 自定义类Person() 里添加一个属性 a = 10
b = 20 # 向 自定义类Person() 里添加一个属性 b = 20
name = '钢铁侠' # 向 自定义类Person() 里添加一个属性 name = '钢铁侠'
def speak(a): # 向 自定义类Person() 里添加一个方法 speak()
print('Hi~')
p_1 = Person()
p_2 = Person()
print(p_1.name) # 钢铁侠
p_2.speak() # Hi~
咦,成功了?这是不是有点 不合常理 哦。最初我们在调用 自定义类Person
里创建 方法函数speak()
出现错误时,我们就复习了一下前面的我们学的有关于 函数 的知识,我们知道:当我们 创建的函数后面 定义几个 形参,我们 调用 的时候就要传递几个 实参;如果我们在 定义函数 的时候没有定义 形参 的话,那我们在 调用该函数 的时候是不是就不用 再传递 进什么 实参 了。可是现在的情况却是:想要正常的调用 自定义类Person
里面的 方法函数,我们就需要定义好 自定义类Person
里面的 方法函数 里的 形参,而我们在调用 自定义类Person
里面的 方法函数 时却不需要传递进 实参。这又是什么情况?这是不是也太反常里了。
这个 反常理 的问题呢,我们在后面的 5. 参数self 着重的再解释一下。这里呢我们先把整个 4. 类的定义 先介绍完,马上就要结束了。
我们调用 自定义类 里面的 方法函数 的语法:对象.方法名()
。
方法调用 和 函数调用 的 区别:
如果是 函数调用,调用函数时 有几个形参,就会传递 几个实参;
如果是 方法调用,自定义类 里 默认 传递进 一个参数,所以 方法 中 至少 得有 一个形参。
到此呢,有关 4. 类的定义 小编我算是简单的介绍完了,接下来我们简单的小结一下:
- 类 和 对象 都是对 现实生活中事物 的 抽象。所谓的 抽象 不是说把什么东西 拓印 出来一个,而是把 某一事物整个 的 特点 和 属性 放到 程序里的一个容器 当中。
- 所有实际的事物都是由 两部分 组成,分别是:1) 数据(属性) 和 2) 行为(方法);
- 在 自定义类 的 代码块 中,我们 可以定义 变量 和 函数。
变量 会成为该 自定义类 实例 的 公共属性,所有由该 自定义类 实例化的对象 都可以通过print(对象.变量)
的形式访问;
函数 会成为该 自定义类 实例 的 公共方法,所有由该 自定义类 实例化的对象 都可以通过对象.方法名()
的形式访问。 - 方法调用 和 函数调用 的 区别:
如果是 函数调用,调用函数时 有几个形参,就会传递 几个实参;
如果是 方法调用,自定义类 里 默认 传递进 一个参数,所以 方法 中 至少 得有 一个形参。
5. 类中的属性和方法
对于 如何 自定义类 以及这 自定义类 有什么 作用,我们已经很详细的讲解过了。接下来呢 我们就要仔细的讲解 类 中的 属性 和 方法 的调用。
参考实例:
class Person():
a = 10 # 向 自定义类Person() 里添加一个属性 a = 10
b = 20 # 向 自定义类Person() 里添加一个属性 b = 20
name = '钢铁侠' # 向 自定义类Person() 里添加一个属性 name = '钢铁侠'
def speak(a): # 向 自定义类Person() 里添加一个方法 speak()
print('Hi~')
p_1 = Person()
p_2 = Person()
print(p_1.name) # 钢铁侠
p_2.speak() # Hi~
我们 自定义 的这个 类,不管是这 自定义类 里面的 属性 还是它里面的 方法 我们都是放在这个 类对象 当中;而且呢,在我们后面的 实例对象 p_1
和 p_2
当中,都没有任何的一个 属性 或者 方法 的。
在讲 类 和 对象 的时候,我们知道:所有的 实例对象 都可以访问 被实例 的 类 里的 任何的一个 属性 或者 方法。此时,我们就要问一个问题了:这个 实例对象 为什么能访问到我们 自定义 的这个 类 里的 属性 或者 方法 呢?这是为什么呀?
看完 问题 后,就有读者朋友就会说了:这个 实例对象 本身就是根据我们 自定义 的这个 类 造出来 的,能访问到 自定义类 里的 属性 或者 方法 这不是正常的吗。
在开始回答大家这个问题之前呢,我们首先要从 根 上明确一个问题,那就是:类 中 定义的 属性 和 方法 都是 公共的,任何该 类 的 实例 都 可以访问。明确这个 根 问题之后呢,我们接下来就开始来聊聊上面的 为什么?
在说这 为什么 之前呢,我们要先了解 自定义类 里的 属性 和 方法 的 查找流程。这个 属性 和 方法 的 查找流程 又该怎么体现呢?
闲言少叙,先拉一个 参考实例:
class Person():
name = '钢铁侠'
p_1 = Person()
print(p_1.name) # 钢铁侠
单看 代码 也看不出什么所以然,我们再拉一个 基于 这段 小代码 在我们 计算机的 内存 当中运转的 思维导图:
通过 参考实例 和 思维导图 我们发现:虽然 参考实例 最后的 print(p_1.name)
打印出了 钢铁侠
,但是我们通过看 思维导图 发现 实例化对象 的 value
里其实 什么都没有。
那如果,我现在想往 实例化对象p_1
的 value
里添加些 属性 我们该怎么操作?是不是 对象.属性名 = 'xxx'
就可以了。
参考实例:
class Person():
name = '钢铁侠'
p_1 = Person()
p_1.name = '蜘蛛侠'
print(p_1.name) # 蜘蛛侠
基于 参考实例 的 思维导图:
通过 参考实例 和 思维导图 我们发现:参考实例 最后打印p_1.name
的结果是 蜘蛛侠
。咦,怎么和最初 参考实例 的结果不一样啊?这 参考实例 最后打印 print(p_1.name)
究竟是怎么一个情况呢?
其实呢 我们打印的 p_1.name
,程序(Python解析器) 会 优先 就近 去 自己的对象当中 找 属性相对应的 值,如果 自己的对象当中 存在 相对应的属性 则 直接返回 相对应的 值;如果 自己的对象当中 没有 相对应的属性值,程序则会再去我们 自定义的 类 中找 相对应的属性值;
class Person():
age = '28'
p_1 = Person()
p_1.name = '蜘蛛侠'
print(p_1.age) # 28
如果在 自定义类 中再找不到 相对应的属性,系统则会返回 AttributeError: 'Person' object has no attribute '……'
错误提示。
class Person():
age = '28'
p_1 = Person()
p_1.name = '蜘蛛侠'
print(p_1.title) # AttributeError: 'Person' object has no attribute 'title'
讲到这,我们是不是又 间接 的回顾了一遍我们之前说的 代码优先级;5.1 类的属性和方法 的调用 和我们之前讲的 算术优先级 、 函数优先级 以及 高级函数 以及 函数的作用域 是不是很相似。😃
讲完了 类 的 属性 和 方法 的 查找优先级 流程之后,新的一个问题就 又来了。是 什么问题呢?那就是:我们什么时候应该把 属性 放在 类对象 当中?什么时候又应该把 属性 放在 实例对象 当中?这是一个 在未来 我们 经常会遇到 的重要问题。
那这 属性 和 方法 我们该怎么存放呀?我们先来看一个问题:一个人 他 最少也必须 要有一个名字吧,而且还是 每个人 都是 最少也必须 要有一个名字吧;但是,每个人 的 名字 在不考虑 重名 的基础前提上,他们又都是 不一样 的 对吧。张三、李四、空、大仙、。、Tor、Tol、アリバ、サイト、한국어、Чернобыль ……不仅不一样,而且还可能是 各种各样 的 对吧。
介绍完了一种 人类 必须有但又必须都不相同的 属性,接下来我们来看那些 人类 必须有且都相同的 属性:在 排除所有特殊问题 的基础上,每个人 他都会 睡觉、说话、行走、吃饭、喝水、喜欢 …… 等等的这些 日常行为操作 对吧。
讲到这,我想大家隐隐的能感觉接下来我会讲什么了。那就是:
类对象 和 实例对象 中 都可以保存 属性 和 方法。
如果这个 属性 或 方法 是 所以实例 共享 的,则应该将其保存到 类对象 中;
如果这个 属性 或 方法 是 某个实例所 独有 的,则应该将其保存到 实例对象 中。
至于这 属性 或 方法 的添加方法,在这里就不再重新的详细描述了;对象.属性 = 'xxx'
和 对象.方法名()'
。
一般情况下,属性 保存到 实例对象 中,实例对象.属性 = 'xxx'
;而 方法 需要保存在 类对象 中。
请注意:我们在这里讲的 属性 和 方法 的 保存方法 讲的是 在一般常规的情况下。以后,在我们参加工作中,如果出现一些 特殊情况 的话我们还要根据 具体的情况 具体处理。
到此呢,有关 5. 类中的属性和方法 小编我算是简单的介绍完了,接下来我们简单的小结一下:
对于在 实例对象 中,调用 属性(方法) 的优先级:
当我们调用一个 对象 的 属性(方法) 时,程序(Python解析器) 会 就近 先在 当前的对象 中寻找是否有 该属性,如果 有,则直接返回 当前对象 的 属性值。如果 没有,则去 当前对象 实例化的类对象 中去寻找,如果 有 则返回 类对象 中的 属性值。如果再没有则会返回 AttributeError: 'Person' object has no attribute '……'
错误提示。
对于 属性 和 方法 一般情况下 的 定义思路:
1) 类对象 和 实例对象 中 都可以保存 属性 和 方法。
2) 如果这个 属性 或 方法 是 所以实例 共享 的,则应该将其保存到 类对象 中;
3) 如果这个 属性 或 方法 是 某个实例所 独有 的,则应该将其保存到 实例对象 中。
4) 一般情况下,属性 保存到 实例对象 中,实例对象.属性 = 'xxx'
;而 方法 需要保存在 类对象 中。
6. 参数self
在开始介绍 参数self 之前呢我们先回顾一下在 4. 类的定义 里留下的一个大问题:
class Person():
a = 10 # 向 自定义类Person() 里添加一个属性 a = 10
b = 20 # 向 自定义类Person() 里添加一个属性 b = 20
name = '钢铁侠' # 向 自定义类Person() 里添加一个属性 name = '钢铁侠'
def speak(a): # 向 自定义类Person() 里添加一个方法 speak()
print('Hi~')
p_1 = Person()
p_2 = Person()
print(p_1.name) # 钢铁侠
p_2.speak() # Hi~
咦,成功了?这是不是有点 不合常理 哦。最初我们在调用 自定义类Person
里创建 方法函数speak()
出现错误时,我们就复习了一下前面的我们学的有关于 函数 的知识,我们知道:当我们 创建的函数后面 定义几个 形参,我们 调用 的时候就要传递几个 实参;如果我们在 定义函数 的时候没有定义 形参 的话,那我们在 调用该函数 的时候是不是就不用 再传递 进什么 实参 了。可是现在的情况却是:想要正常的调用 自定义类Person
里面的 方法函数,我们就需要定义好 自定义类Person
里面的 方法函数 里的 形参,而我们在调用 自定义类Person
里面的 方法函数 时却不需要传递进 实参。这又是什么情况?这是不是也太反常里了。
接下来呢,我们就一起来好好的研究研究:为什么 会出现这样的情况呢?
参考实例里的 自定义类 我就不再仔细的介绍了,在 4. 类的定义 里面已经讲烂了。参考实例 中我们先把 代码def speak(self):
里的 self
改为 a
,我们说 函数def speak(a):
里面有个 参数a
,我们在调用这个 类对象 的 方法函数 的时候,Python语言的解析器 会 默认 的来传递这么一个 形参a
;所以,我们以后写 类对象 的 方法函数 的时候, 方法函数 的后面至少要定义一个 形参。至于说这个 形参self
有什么作用,我们现在先别管,到了后面我们会再详细的解释的;我们先来看一个 参考实例:
# 让 p_1.speak() 和 p_2.speak() 最后的输出结果分别是:'你好,我是蜘蛛侠' 和 '你好,我是绿巨人' 。
class Person():
name = '钢铁侠' # 向 自定义类Person() 里添加一个属性 name = '钢铁侠'
def speak(a): # 向 自定义类Person() 里添加一个方法 speak()
print('你好')
p_1 = Person()
p_2 = Person()
p_1.name = '蜘蛛侠'
p_2.name = '绿巨人'
p_1.speak() # 你好
p_2.speak() # 你好
通过 参考实例 我们发现,我们的需求是 需要 让程序的 p_1.speak()
和 p_2.speak()
最后的输出结果分别是:‘你好,我是蜘蛛侠’ 和 ‘你好,我是绿巨人’ 。单看我们的需求还是很简单的吧,但是,程序运行后 最后 实际的输出结果 却分别只是:你好
和 你好
。
如果我们修改一下 方法函数def speak(a):
里输出参数;我们一起来看下最后的输出结果。
参考实例:
class Person():
name = '钢铁侠'
def speak(a):
print('你好,我是蜘蛛侠')
p_1 = Person()
p_2 = Person()
p_1.name = '蜘蛛侠'
p_2.name = '绿巨人'
p_1.speak() # 你好,我是蜘蛛侠
p_2.speak() # 你好,我是蜘蛛侠
通过 参考实例 我们发现,程序运行后 最后 实际的输出结果 却分别是:你好,我是蜘蛛侠
和 你好,我是蜘蛛侠
。而且我们还发现:程序最后之所以 只输出 你好,我是蜘蛛侠
实际原因是因为我们把 方法函数def speak(a):
最后的输出结果 写死了 呀,这么 写的代码 是不是违反了 程序设计的 开闭原则(OCP)。
那假如我们把 方法函数def speak(a):
里输出参数再修改一下;我们一起来看下最后的输出结果。
参考实例:
class Person():
name = '钢铁侠'
def speak(a):
print('你好,我是%s'%'绿巨人')
p_1 = Person()
p_2 = Person()
p_1.name = '蜘蛛侠'
p_2.name = '绿巨人'
p_1.speak() # 你好,我是绿巨人
p_2.speak() # 你好,我是绿巨人
通过 参考实例 我们发现,程序运行后 最后 实际的输出结果 却分别是:你好,我是绿巨人
和 你好,我是绿巨人
。而且我们还发现:虽然我们把 方法函数def speak(a):
最后的输出结果使用了 占位符 进行拆分,其实实际上也是 间接 的把方法函数def speak(a):
最后的输出结果写死了 呀。在上一个 参考实例 里我们说过:这么 写的代码 违反了 程序设计的 开闭原则(OCP)。
既然说,我们的前两个操作都是直接或者间接的把 方法函数def speak(a):
里输出参数 写死了;那假如我们把 方法函数def speak(a):
里输出参数修改为 print('你好,我是%s'%name)
;我们一起来看下最后的输出结果。
class Person():
name = '钢铁侠'
def speak(a):
print('你好,我是%s'%name)
p_1 = Person()
p_2 = Person()
p_1.name = '蜘蛛侠'
p_2.name = '绿巨人'
p_1.speak()
p_2.speak()
# NameError: name 'name' is not defined
很好,很优秀,很卓越。 这次的修改最后的 输出结果 直接就 报错 了 NameError: name 'name' is not defined
。访问的变量名不存在: 名字为 ‘名字’ 的变量未定义。
在这里有个 注意点:参考实例 里的 自定义类 里的 方法 它不是个 函数闭包。如果只是一个 函数闭包 是可以访问到 变量name = '钢铁侠'
的,但我们现在是在一个 自定义类 里对它的 方法 进行操作;自定义类 里的 方法 是 无法访问 到 自定义类 里的 属性 的。
def Person():
name = '钢铁侠'
def speak():
print('你好,我是%s'% name)
return speak
p_1 = Person()
print(p_1()) # 你好,我是钢铁侠
# None
很明显,上述三种操作都无法通过我们的 自定义类Person
来实现需求:让程序的 p_1.speak()
和 p_2.speak()
最后的 输出结果 分别是:你好,我是蜘蛛侠
和 你好,我是绿巨人
。那我们该如何正确的使用我们的 自定义类Person
来实现甲方爸爸对我们的需求呢?
其实,抛开一切,我们 只是希望 代码print('你好,我是%s'% name)
里的 name
访问到是p_1.name = '蜘蛛侠'
和 p_2.name = '绿巨人'
。那我们该怎么操作?
参考实例:
class Person():
name = '钢铁侠'
def speak(a):
print('你好,我是%s'% p_1.name)
p_1 = Person()
p_2 = Person()
p_1.name = '蜘蛛侠'
p_2.name = '绿巨人'
p_1.speak() # 你好,我是蜘蛛侠
p_2.speak() # 你好,我是蜘蛛侠
虽然,最后 p_1.speak()
和 p_2.speak()
的 输出结果 还是一样的,但是程序 最后的输出结果 不是通过 写死输出结果 来实现的。最起码,在编码上我们 符合 了 程序设计的 开闭原则(OCP)。
我们再把 代码print('你好,我是%s'% p_1.name)
里的 p_1
改为 p_2'
。让我们再来看看最后的 操作结果。
参考实例:
class Person():
name = '钢铁侠'
def speak(a):
print('你好,我是%s'% p_2.name)
p_1 = Person()
p_2 = Person()
p_1.name = '蜘蛛侠'
p_2.name = '绿巨人'
p_1.speak() # 你好,我是绿巨人
p_2.speak() # 你好,我是绿巨人
虽然在编码上我们 符合 了 程序设计的 开闭原则(OCP)。但是程序 最后的输出结果 还是要通过我们手动对 p_1
和 p_2
进行修改的。所以,如果真的要 计较 的的话,其实上面的 参考实例 还是不符合 程序设计的 开闭原则(OCP) 的。
其实,我们希望 谁调用 最后就输出 谁的名字,那我们的 代码 该怎么编写才能让程序 最后的输出结果 符合我们的需求?我们的 代码 该怎么编写才能让最终的结果符合 程序设计的 开闭原则(OCP)?
那么 新的问题 就来了:我怎么知道 谁在调用 呢?
我们当然是无法知道的了。我们现在只是在我们的 自定义类 里定义了 def speak(a):
这么一个 方法函数,我们在定义 方法函数 的时候是 无法确定 最后是谁会 需要调用 该方法的。此时我们看到这个 自定义类 里的 方法函数 def speak(a):
里有个 a
,它又能干什么?
老方法:遇事不清就 打印。我们现在就先来 打印 这个 a
,看看最后输出的结果。
class Person():
name = '钢铁侠'
def speak(a):
print(a)
p_1 = Person()
p_1.name = '蜘蛛侠'
p_1.speak() # <__main__.Person object at 0x0000027ACC628400>
我们现在 打印 这个 a
是不是只需要调用 p_1.speak()
把这个 方法输出就可以了。最后调用输出的这个结果为:<__main__.Person object at 0x0000027ACC628400>
这是个什么呀……<主文件.Person 对象 在内存 0x0000027ACC628400>
。
简单的讲:我们打印的这个 a
最后返回输出的是一个 对象。
此时,我们再 打印 一下我们的 实例化对象p_1
,看看 打印 它最后会输出一个什么结果:
class Person():
name = '钢铁侠'
def speak(a):
print(a)
p_1 = Person()
p_1.speak() # <__main__.Person object at 0x0000027ACC628400>
print(p_1) # <__main__.Person object at 0x0000027ACC628400>
咦,怎么会这样? 通过最后返回的 打印 结果我们发现:我们对 实例化对象p_1
的 打印结果 和 我们前面 打印 的 自定义类 里的 方法函数 def speak(a):
里的形参 a
的 打印结果 竟然完全的一模一样,连所在的 内存地址值 都一样。会不会有什么问题?我们尝试着再打印一次看看:
class Person():
name = '钢铁侠'
def speak(a):
print(a)
p_2 = Person()
p_2.speak() # <__main__.Person object at 0x000002A08C2A8400>
print(p_2) # <__main__.Person object at 0x000002A08C2A8400>
我们再一次看到 实例化对象p_2
的 打印结果 和 我们前面 打印 的 自定义类 里的 方法函数 def speak(a):
里的形参 a
的 打印结果 竟然也同时指向一个对象。
如果我们分别通过 实例化对象p_1
和 p_2
打印 自定义类 里的 方法函数 def speak(a):
里的形参 a
最后会返回什么结果呢?打印 一下看看不就知道了。
class Person():
name = '钢铁侠'
def speak(a):
print(a)
p_1 = Person()
p_2 = Person()
p_1.speak() # <__main__.Person object at 0x00000270C6B68400>
print(p_1) # <__main__.Person object at 0x00000270C6B68400>
p_2.speak() # <__main__.Person object at 0x00000270C6B8BF70>
print(p_2) # <__main__.Person object at 0x00000270C6B8BF70>
这 打印 的是 同一个 自定义类 里的 方法函数 def speak(a):
里的形参 a
为什么最后返回的指向的结果还 不一样了呢?
此时,读者朋友们你们有没有发现什么 规律 啊?此时我们是不是发现:如果 实例化对象p_1
在调用 自定义类 里的 方法函数 def speak(a):
,那么 方法函数 def speak(a):
里的形参 a
是不是就是 p_1
;如果 实例化对象p_2
在调用 自定义类 里的 方法函数 def speak(a):
,那么 方法函数 def speak(a):
里的形参 a
是不是就是 p_2
。
既然发现了这个规律,那么对 自定义类 里的 方法函数 def speak(a):
的形参 a
或者说是 形参 self
进行一个 总结:
自定义类 里的 方法函数 每次 被调用 时,解析器 都会 自动 传递一个参数;
传递的 第一个参数 就是 调用方法的 本身。
如果是 p_1 调用,第一个参数就是 p_1 对象;
同理,如果是 p_2 调用,第一个参数就是 p_2 对象。
即然现在我总结出了上面这个结论,那么 回到最初 :对 让程序的 p_1.speak()
和 p_2.speak()
最后的输出结果分别是:你好,我是蜘蛛侠
和 你好,我是绿巨人
这个需求,此时我们应该怎么写?
参考实例:
# 让 p_1.speak() 和 p_2.speak() 最后的输出结果分别是:'你好,我是蜘蛛侠' 和 '你好,我是绿巨人' 。
class Person():
name = '钢铁侠'
def speak(self):
print('你好,我是%s'% self.name)
p_1 = Person()
p_2 = Person()
p_1.name = '蜘蛛侠'
p_2.name = '绿巨人'
p_1.speak() # 你好,我是蜘蛛侠
p_2.speak() # 你好,我是绿巨人
最后呢,对于前面的 总结 再做最后一点的 补充:自定义类 里的 方法函数 的 第一个参数 一般我们习惯把这个参数命名为 self。
下一次,我们再在别的程序或这项目的类对象中看到 self 时,希望大家别还是 一脸懵 哦。如果担心自己可能还是会 一脸懵 的话,那就请你 点击“收藏” 下这篇文章吧。
到此呢,有关 6. 参数 self 小编我算是简单的介绍完了,接下来我们简单的小结一下:
1) self 在 定义函数 时 需要定义,但是在 调用 时会 自动传入;
2) 自定义类 里的 方法函数 每次 被调用 时,解析器 都会 自动 传递一个参数;
3) 传递的 第一个参数 就是 调用方法的 本身。
4) self 的 名字 并不是 规定死 的,但是最好还是 按照约定是用 self;
5) self 总是指 调用时 的 类的实例。
总结小便条
本篇文章主要讲了以下几点内容:
- 什么是 面向对象?
答:为了方便程序员 快捷的开发 和 方便的维护,以及代码的 灵活性 和 复用性;我们通过传统的 面向过程 的编程方式衍化出了 面向对象 的编程方式。
面向对象 的编程方式虽然方便了程序员 快捷的开发 和 方便的维护,也涵盖了代码的 灵活性 和 复用性,但是编写的过程中 不太符合 常规的思维方式,刚开始编写 的时候相对的会比较麻烦。 - 面向对象 中的 类 到底是什么?
答: 类 简单的理解 就是一张 图纸,在程序中我们需要根据 类 来 创建对象;类 就是对象的 图纸。自定义类 的 语法:class ClassName:
。
class MyClass():
pass
print(MyClass,type(MyClass)) # <class '__main__.MyClass'> <class 'type'>
- 类 该怎么使用:
答:通过 自定义变量 将它 实例化。
class MyClass:
pass
mc = MyClass()
mc.name = '蜘蛛侠'
mc_1 = MyClass()
mc_1.name = '钢铁侠'
print(mc.name) # 蜘蛛侠
print(mc_1.name) # 蜘蛛侠
-
现实开发中,对 类 在定义时,我们需要注意哪些 细节 和 要素:
答:- 类 和 对象 都是对 现实生活中事物 的 抽象。所谓的 抽象 不是说把什么东西 拓印 出来一个,而是把 某一事物整个 的 特点 和 属性 放到 程序里的一个容器 当中。
- 所有实际的事物都是由 两部分 组成,分别是:数据(属性) 和 行为(方法);
- 在 自定义类 的 代码块 中,我们 可以定义 变量 和 函数。
变量 会成为该 自定义类 实例 的 公共属性,所有由该 自定义类 实例化的对象 都可以通过print(对象.变量)
的形式访问;
函数 会成为该 自定义类 实例 的 公共方法,所有由该 自定义类 实例化的对象 都可以通过对象.方法名()
的形式访问。 - 方法调用 和 函数调用 的 区别:
如果是 函数调用,调用函数时 有几个形参,就会传递 几个实参;
如果是 方法调用,自定义类 里 默认 传递进 一个参数,所以 方法 中 至少 得有 一个形参。
-
在 实例对象 中,调用 属性(方法) 的优先级:
答:当我们调用一个 对象 的 属性(方法) 时,程序(Python解析器) 会 就近 先在 当前的对象 中寻找是否有 该属性,如果 有,则直接返回 当前对象 的 属性值。如果 没有,则去 当前对象 实例化的类对象 中去寻找,如果 有 则返回 类对象 中的 属性值。如果再没有则会返回AttributeError: 'Person' object has no attribute '……'
错误提示。 -
属性 和 方法 一般情况下 的 定义思路:
答:类对象 和 实例对象 中 都可以保存 属性 和 方法。
如果这个 属性 或 方法 是 所以实例 共享 的,则应该将其保存到 类对象 中;
如果这个 属性 或 方法 是 某个实例所 独有 的,则应该将其保存到 实例对象 中。
一般情况下,属性 保存到 实例对象 中,实例对象.属性 = 'xxx'
;而 方法 需要保存在 类对象 中。 -
解释一下 参数self 的意义。
答:1) self 在 定义函数 时 需要定义,但是在 调用 时会 自动传入;
2) 自定义类 里的 方法函数 每次 被调用 时,解析器 都会 自动 传递一个参数;
3) 传递的 第一个参数 就是 调用方法的 本身。
4) self 的 名字 并不是 规定死 的,但是最好还是 按照约定是用 self;
5) self 总是指 调用时 的 类的实例。
本章回顾暂时就到这了,如果还有点晕,那就把文章里所有引用的案例代码再敲几遍吧。拜拜~