面向对象相关知识背景
面向对象即Object Oriented,OO,这是一种软件开发方法,具体的定义也说不清楚,根据书中的描述,对象大致意味着一系列数据或属性以及一套访问这些数据的方法。在《Python无师自通》这本书中提到:
在 Python 中,每一个数据值,如 2 或”Hello, World!”,被称为对象(object)。可以把对象看作拥有 3 个属性的数据值:唯一标识(identity)、数据类型和值。
对象的唯一标识,指的是其在计算机内存中的地址,该地址不会变化。
对象的数据类型是对象所属的数据类别,这决定了对象的属性,也不会变化。
对象的值是其表示的数据,例如数字 2 的值即为 2。
“Hello, World!”这个对象的数据类型为字符串(str,string 的缩写),值为”Hello, World!”。如果提及数据类型为 str 的对象,可以称其为字符串。
与面向对象有关的一些术语有多态、封装、方法、属性、超类和继承。
多态:可对不同类型的对象执行相同的损伤;;
封装:对外部隐藏有关对象工作原理的细节。
继承:可基于通用类创建出专用类。
在后面的笔记中会详细说明这是什么意思。
什么是类
类是一种对象,每个对象都属于特定的类,并被称为该类的实例。例如,如果你在窗外看到一只鸟,这只鸟就是“鸟类”的一个实例,鸟类是一个非常通用(抽象)的类,它有许多个子类:你看到的那只鸟可能属于子类”云雀“。你可以将“鸟类”视为由所有鸟组成的集合,而“云雀”是其一个子集,一个类的对象为另一个类的对象的子集时,前者就是后者的子类。因此“云雀”为“鸟类”的子类,而“鸟类”为“云雀”的超类。并且在面向对象的编程中,是先定义类,然后再由类生成一个实例。
通过上面的比喻,我们大致就理解了类,子类,超类。但在面向对象的编程中,子类可能还要更复些,因为类是由其支持的方法定义的。类的所有实例都有该类的所有方法,因此子类的所有实例都有超类的所有方法。因此,要定义子类,只需要定义多出来的方法(或者是重写某个方法)即可。例如鸟类(我们用Bird替代)可能提供方法fly,而Penguin类(Bird的一个子类)可能新增方法eat_fish。创建Penguin类时,我们还可能要重写超类方法,即方法fly,企鹅Penguin不会飞,我们要在Penguin的实例中,方法fly应什么都不做或引发异常。
创建类
在使用面向对象开发之前,应该首先分析一下需要,确定程序中需要包括哪些类。在程度开发中,要设计一个类,通常需要满足以下三个要求:
- 类名。类名的命名要以驼峰式命名法进行命名,也就是每个单词的首字母要大写,例如
CapWords
; - 属性。属性赋予事物具有什么样的特性;
- 方法。方法赋予事物具有什么样的行为。
封装(encapsulation)
封装指的是向外部隐藏不必要的细节。这听起来有点像多态(无需知道对象的内部细节就可使用它)。这两个概念很像,因为它们都是抽象的原则。它们都像函数一样,可帮助你处理程序的组成部分,让你无需关心不必要的细节。
但封装与多态又有所不同,多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封闭则是让你需知道对象的构造就能使用它,下面展示一个案例,在这个案例中,使用了多态,但是没有使用封闭,发吭你有一个名为OpenObject的类,如下所示:
|
|
在上面的案例中,我们像调用函数一样调用了类,创建了一个对象,并将其关联到变量o,然后使用了set_name
和get_name
这两个方法(假设OpenObject支持这些方法),但是,如果o将其名称储存在全局变量global_name中,如下所示:
|
|
这就意味着使用OpenObject类的实例(对象)时,就需要考虑global_name的内容,事实上,必须确保无人能修改它。
|
|
d如果浓度创建多个OpenObject对象,则会出现问题,因为它们共用同一个变量,如下所示:
|
|
在上面的这个案例中,设置一个对象的名称时,将自动设置另一个对象的名称,这可能不是我们想要的结果。基本上,你希望对象是抽象的:当调用方法时,无需操心其他的事情,如避免干扰全局变量。如何将名称“封装”在对象中呢,将其作为一种属性
即可,属性是归属于对象的变量,就像方法一样,实际上,方法差不多就是与函数相关联的属性,如果你使用属性而非全局变量重新编写前面的类,并将其重命名为ClosedObject,就可以像下面这样使用它,如下所示:
|
|
到目前为止,一切顺利,但这并不能证明名称不是存储在全局变量中的,现在再来创建一个对象,如下所示:
|
|
从中可知正确地设置了新对象的名称,此时再来看一下第一个对象,如下所示:
|
|
其名称还在,因为这个对象有自己的状态,对象的状态由其属性(如名称)描述,对象的方法可能修改这些属性,因此对象将一系列函数(方法)组合起来,并赋予它们访问一些变量(属性)的权限,而属性可用于在两次函数调用之间存储值。
属性和方法的确定
描述对象特征的内容可以定义为属性
。对象具有的某些行为(动词),就可以定义为方法
。
例如我们看下面一个案例:
- 小明今年18岁,身高1.75,每天早上跑完步,会去吃东西。
- 小美今年17岁,身高1.65,小美不跑步,小美喜欢吃东西。
从上面的描述我们可以这么思考,我们可以设计一个人类,例如persion
,这个类中需要包含3个属性,即名字name
,年龄age
,和身高height
,此外,还要包括2个动作,分别为是跑步run
和吃东西eat
。
再看一个案例:
我们有以下需求:
- 一只黄颜色的狗,叫大黄
- 看见生人叫
- 看见家人摇尾巴
那么我们就会设计这样一个狗类(Dog
),这个类中含有2个属性,分别是名字(name
)用于记录狗的名字,颜色(color
)用于记录狗的颜色;含有2个方法,分别是叫(shout
)和摇尾巴(shake
)。
以上就是类是如何设计的,总之就是一句话,先考虑需求,然后考虑类,描述性的文字是属性,动作性的文字是方法。
面向对象基础语法
在Python中,对象无所不在,变量、数据、函数都是对象。在Python中,可能通过两种方法来验证以对象:
- 在标记符/数据后输入一个
.
,然后按下TAB
键,ipython
就会提示该对象能够调用的方法列表
。 - 使用内置函数
dir
传入标识符/数据,可以查看对象内的所有属性及方法。
先看第一种方法,在ipython
中定义一个列表变量,即gl_list=[]
,然后输入gl_list.
(后面有一个点),按下TAB
键,后面就会出现这个对象能使用的方法,如下所示:
|
|
现在定义一个函数,如下所示:
|
|
此时输入demo后面再加一个点.
,没有任何信息,此时就需要用另外一种方法来验证它是一个对象,也就是使用dir
,如下所示:
|
|
从上面的我们可以看出来,这里面是一个列表,它列出了很多方法,这些方法都是双下划线开头,双下划线结尾,这些东西都是Python内置的方法(__方法名__
),有些可以直接使用,例如__doc__
,如下所示:
|
|
此时我们就可以看到,使用__doc__
这个内置方法就能查看函数的文档说明,常用的一些内置方法/属性有以下这些:
序号 | 方法名 | 类型 | 作用 |
---|---|---|---|
1 | __new__ |
方法 | 创建对象时,会被自动调用 |
2 | __init__ |
方法 | 对象被初始化时,会被自动调用 |
3 | __del__ |
方法 | 对象被从内存中销毁之前,会被自动调用 |
4 | __str__ |
方法 | 返回对象的工描述信息,print函数输出使用 |
因此,dir()
这个函数很有用,例如当我想调用某个函数的方法时,一时想不想来,就可以使用dir()
这个函数。
定义简单的类
格式
在Python中要定义一个只包含方法的类,格式如下:
|
|
先看下面的一个案例:
|
|
在上面的这个案例中,一共定义了3种方法,它们类似于函数定义,但都位于class语句语,Person是类的名称。class语句创建独立的命名空间,用于在其中定义方法(其实就是定义函数)。self它指向对象本身,具体是哪个对象呢,我们再来看下面的案例:
|
|
运行结果如下所示:
|
|
这个案例主要是用来说明self是什么,对foo调用set_name和greet时,foo都会做为第一个参数自动传递给它们,也就是命名为self的原因(其实可以任意命名),如果没有self,所有的方法都无法访问对象本身,也就是要损伤的属性所属的对象。此外,也可以从外部访问这些属性,如下所示:
|
|
运行结果如下所示:
|
|
属性、函数和方法
方法和函数的区别表现在前面提到的参数self上,方法更准确地说是关联的方法,是将第一个参数关联到它所属的实例中,因此无需提供这个参数。也可以将属性关联到一个普通的函数,但这样就没有特殊的self参数了,如下所示:
|
|
运行结果如下所示:
|
|
从上面可以知道,有没有参数self并不取决于是否以刚才使用的方式(如instance.method)调用方法。实际上,完全可以让另一个变量指向同一个方法,如下所示:
|
|
运行结果如下所示:
|
|
最后一个方法调用看起来很像函数调用,但变量birdsong指向的是关联的方法bird.song,这意味着它也能够访问参数self(即使它被关联到类的实例)。
隐藏
默认情况下,我们可以从外部访问对象的属性,看下面的案例:
|
|
如果我们不让其他人从外部访问属性,就可能将属性定义为私有,私有属性不能从对象外部访问,而只能通过存取器(例如get_name和set_name)来访问。
要让方法或属性成为私有的(不能从外部访问),只需要让其名称以两个下划线打头即可,如下所示:
|
|
运行结果如下所示:
|
|
从上面的案例我们可以看出来,无法从外部访问__inaccessible
,但是在类中,例如accessible
中我们仍然可以使用它。虽然以两个下划线打头有点奇怪,但这样的方法类似于其他语言中的标准私有方法。然后背后的处理手法并不标准:在类定义中,对所有以两个下划线打头的名称都进行转换,即在开头加上一个下划线和类名,如下所示:
|
|
上面的代码在我的编辑器中无法运行,可以直接跳过,但知道了这种幕后处理手法,就能从类外部访问私有方法,但是不建议这么做,如下所示:
|
|
运行结果如下所示:
|
|
总之,你无法禁止别人访问对象的私有方法和属性,但这种名称修改方式发出了强烈的信号,让他们不要这样做。如果你不希望名称被悠,又想发出不要从外部修改属性或方法的信号,可以用一个下划线打头,这虽然只是一种约定,但也有些作用,例如使用from module import *
就不会导入以一个下划线打头的名称。
类的命名空间
下面的两条语句大致等价:
|
|
它们𨝌会创建一个返回参数平方的函数,并将这个函数关联到变量foo,可以在全局(模块)作用域内定义名称foo,也可以在函数或方法内定义。定义类时情况也是如此,在class语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,而类的所有成员都可以访问这个命名空间。类定义其实就是要执行的代码段。例如,在类定义中,并非只能包括def语句,如下所示:
|
|
这个案例非常简单,再看下一个案例:
|
|
运行结果如下所示:
|
|
上述代码在类作用域内定义了一个变量,所有的成员(实例)都可以访问它,这里使用它来计算类实例的数量,注意到这里使用了init来初始化所有的实例,也就是将init转换为合适的构造函数,每个实例都可以访问这个类作用域内的变化,就像方法一样,如下所示:
|
|
如果在一个实例中给属性members赋值,结果如下所示:
|
|
新值被写入m1的一个属性中,这个属性遮住了类的变量。
创建对象
当一个类定义完全成,要使用这个类来创建对象,语法格式如下:
|
|
创建对象
先看一个案例:例如小猫爱吃鱼,小猫要喝水。
分析:
- 定义一个猫类,Cat()
- 定义两个方法
eat
和drink
- 此时并没有涉及属性,因此我们不用定义属性。
|
|
运行结果如下所示:
|
|
在这个案例中,我们先定义了一个猫类(Cat),然后创建了一个猫对象,再后,使这个对象赋予了吃与喝这两个方法,这两个方法已经封装到了类中,并不需要知道它的执行细节。
如果要用print
函数直接输出对象,那么输出结果就是此对象创建的内存地址,如下所示:
|
|
创建多个对象
类只有一个,类只是一个模板,使用这个模板可以创建多个对象,如下所示:
|
|
我们来看一下结果:
|
|
从结果可以看出来,tom与lazy_cat这两个对象的内存地址不一样,它们是不一样的对象,再看一下以下代码:
|
|
结果如下:
|
|
可以发现,lazy_cat和lazy_cat2这两个对象是一样的。
方法中的self参数
给对象添加属性
在Python中很容易给对象添加属性,但是这种做法并不推荐,因为对象的属性通常都已经封装到类中了,没必要再单独给对象添加属性,虽然不推荐,但还是要看一下案例,如下所示:
|
|
输出对象属性
前面是找到,在类中定义的方法中的一个参数self
,这个self是指:哪一个对象调用的方法,self就是哪一个对象的引用。
例如代码中的tom = Cat()
,tom指向的对象就是由Cat这个类创建的,此时这个对象调用了Cat中的eat这个方法时,self指的也是这个对象,如果要想访问这个对象的属性,那么就是self加一个点(.
)即可,如下所示:
|
|
结果如下所示:
|
|
我们可以看到,原来的“小猫”就被替换成了“Tom”和“大懒猫”。也就是说,由哪一个对象调用的方法,方法内的self就是哪一个对象的引用。在类封装的方法内部,self就表示当前调用方法的对象自己。调用方法时,程序员不需要传递self参数。在方法内部,可以通过self.
访问对象的属性,也可以通过self.
调用其它的对象方法。
初始化方法
当使用类名()
创建对象时,会自动执行以下操作:
- 为对象在内存中分配空间——创建对象
- 为对象的属性设置初始值——初始化方法(init)
这个初始化方法就是__init__
方法,__init__
是对象的内置的方法,此方法是专门用来定义一个类具有哪些属性的方法,这个方法是固定的。我们在Cat
中增加__init__
方法,验证此方法在创建对象时会被自动调用,如下所示:
|
|
运行结果如下所示:
|
|
从结果我们可以发现,使用类名()来创建对象的时候,会自动调用初始化方法__init__
。
在初始化方法内部定义属性
在__init__
方法内部使用self.属性名 = 属性的初始值
就可以定义属性
定义属性之后,再使用Cat类创建的对象都会拥有该属性,如下所示:
|
|
运行结果,如下所示:
|
|
现在解释一下上面的代码,代码是从上到下执行的:
- 当Python解释器遇到
class Cat:
时,这段代码是不执行的,直接跳过去,跳到tom=Cat()
处; - 我们的代码运行到
tom=Cat()
处时,Python解释器此时会做两件事情:①在内存中为Cat对象分配一块空间,假设就是0x1234这块空间(16进制);②然后执行class Cat
这块的代码,类代码中的self
也会指向这块空间(即0x1234),此时也是从上到下执行的,执行到self.name = "Tom"
时,这块空间就被命名为了Tom,因此在使用print(tom.name)
时,就输出了空间的名字。
初始化方法的改造
在前面代码的基础上,我们再创建一个对象,lazy_cat
,如下所示:
|
|
从上面代码我们可知,lazy_cat
这个对象的名称还是Tom
,如下所示:
|
|
运行后,发现果然如此。因为我们在初始化时,已经把对象的名称固定了,就是self.name = "Tom"
这句代码。现在我们要解决这个问题。
如果要解决这个问题,我们需要在初始化方法中再定义一个形参,用于输入不同对象的名称,现在我们在def __init__(self)
中添加一个参数,new_name,即def __init__(self, new_name)
,当我们已经添加了这个形参后,在原来代码创建一个新对象时,也要添加相应的实参,例如tom = Cat()
就需要写为tom= Cat("Tom")
,完整代码如下所示:
|
|
运行结果如下所示:
|
|
此时我们就发现了,一个对象对应一个名称。
总结一下就是,在开发中,如果希望在创建对象的同时就设置对象的属性,就可以对__init__
方法进行改造:
- 把希望设置的属性值,定义成
__init__
方法的参数; - 在方法内部使用
self.属性 = 形参
接收外部 传递的参数; - 在创建对象时,使用
类名(属性1,属性2...)
调用。
内置方法和属性
这一部分介绍两个内置方法,如下所示:
序号 | 方法名 | 类型 | 作用 |
---|---|---|---|
01 | __del__ |
方法 | 对象被从内存中销毁之前,会被自动调用 |
02 | __str__ |
方法 | 返回对象的描述信息,print函数输出使用 |
__del__
方法
- 在Python中,当使用
类名()
创建对象时,为对象分配完空间后,自动调用__init__
方法。 - 当一个对象被从内存中销毁之前,会自动调用
__del__
方法。
应用场景
__init__
改造初始化方法,可以让创建对象更加灵活。__del___
如果希望在对象被销毁之前,再做一些事情,可以使用__del__
方法。
生命周期
- 一个对象从调用
类名()
创建,生命周期开始。 - 一个对象的
__del__
方法一旦被调用,生命周期结束。 - 在对象的生命周期内,可以访问对象属性,或者让对象调用方法。
先来看一个案例,代码如下所示:
代码A:
|
|
结果如下所示:
|
|
现在,我们将代码中的del tom
显示出来,如下所示:
代码B:
|
|
运行结果如下所示:
|
|
从结果中我们可以发现,代码A中的运行结果中,Tom 我去了
出现在了点线下面,而代码B中的Tom 我去了
则出现在了点线上面。而代码A与代码B的区别就在于,代码A中没有del tom
这段代码,而代码B中有。
代码A中没有del tom
这段代码,就说明,只有Python把tom这个对象自动删除(不是指操作者自己操作)后,才会出现__del__
方法中的字符。我猜测这可能是Python自动回收内存的一种机制(注:关于Python的内存回收机制我也不太懂,有空了再补上)。而如果你自己亲自操作,也就是说使用了del tom
这个代码后,Python就知道你把tom这个对象删除了,就开始运行__del__
方法中的内容,然后再运行print("-"*50)
这段代码。
__str__
方法
在Python中,使用
print
输出对象变量,默认情况下,会输出这个变量引用的对象是由哪一个类创建的对象,以及在内存中的地址(十六进制表示)。如果在开发中,希望使用
print
输出对象变量时,能够打印自定义的内容,就可以利用__str__
这个内置方法了。需要注意的是,__str__
方法必须返回一个字符。
看下面的案例,在这个案例,我们直接输出一个对象(有的教程在此处称之为“实例”),如下所示:
|
|
运行结果如下所示:
|
|
其中第二行,即<__main__.Cat object at 0x0000021E1311B278>
这里输出的是Cat这个类,并把tom这个对象在内存中的地址显示出来。现在我们在类中增加__str___
这个方法,如下所示:
|
|
运行结果如下所示:
|
|
此时,我们改造了__str__
这个方法后,输出的就不再是这个对象的类,以及这个对象的地址了。而是我们自定义的内容,现在把__str__
中的方法再改造一下,改成return "我是小猫[%s]"% self.name
,则结果就如下所示:
|
|
面向对象封装(encapsulation)案例
封装
- 封装是面向对象编程的一大特点;
- 面向对象编程的第一步就是将属性和方法封装到一个抽象的类中;
- 外界使用类创建对象(有的教程叫实例),然后让对象调用方法;
- 对象方法中的细节都被封装在类的内部。
案例分析——小明爱跑步
我们以“小明爱跑步”为例说明一下,先分析需求,如下所示:
- 小明体重75.0公斤;
- 小明每次跑步会减肥0.5公斤;
- 小明每次吃东西体重增加1公斤。
代码分析
代码如下所示:
|
|
运行结果如下所示:
|
|
如果我们把Person
这个类折叠起来,就是下面的这个样子,如下所示:
|
|
从上面折叠后的代码我们就知道,我们创建了一个Person
这个类,再由这个类创建了一个叫xiaoming
的对象,然后这个对象进行了跑步(run
)和吃东西(eat
),具体这跑步与吃东西它们的代码,都已经封装到了Person
这个类中,我们直接调用即可。
案例扩展——小美也爱跑步
在原来案例的基础上进行扩展,先看一下需要:
- 小明和小美都爱跑步;
- 小明体重75.0公斤;
- 小美体重45.0公斤;
- 每次跑步都会减少0.5公斤;
- 每次吃东西都会增加1公斤。
代码分析——案例扩展
再使用Persion
这个类创建一个新对象,即xiaomei
,如下所示:
|
|
结果如下所示:
|
|
我们可以看到,由一个类创建的两个对象。在每个对象的方法内部,可以直接访问对象的属性。每个对象各自使用各自的方法,属性互不影响。
案例分析三——摆放家具
现在我们再看一个案例,摆放家具,看一下需求:
- 房子(House)有户型、总面积和家具名称列表,而新房子没有任何家具;
- 家具(HouseItem)有名字和占地面积,其中不同的家具占地面积也不一样,例如床(bed)占地4平方米,衣柜(chest)占地2平方米,餐桌(table)占地1.5平方米;
- 现在我们要将2中的3样家具添加到房子中;
- 输出房子时,要求输出这些信息:户型、总面积、剩余面积和家具名称列表。
有了上面需求后,我们要考虑一下如何设计代码:
- 定义2个类,一个是房子(House),一个是家具(HouseItem)。
- 房子有4个属性;
- 家具有2个属性。
但是,房子与家具这两个类先定义哪个?思路就是,由于房子这个类中要用到家具(家具有面积,房子的剩余面积与家具有关),因此我们要先定义家具这个类,总之就是,被使用的类要先定义。
定义家具类
根据前面的分析思路,现在先定义一个家具类,如下所示:
|
|
结果运行如下所示:
|
|
定义房子类
思路:房子中需要定义4个属性,分别为房子类型(house_type),面积(area),剩余面积(free_area),家具列表(item_list)。但是,在给房子传递参数时,只需要传递其中的2个即可,分别是房子类型(house_type)与面积(area),因为剩余面积(free_area)可以由这2个参数算出来,而家具列表(item_list)在初始情况下是一个空列表,,也不需要传入。
注:在同一个代码文件中,如果要定义2个以及2个以上的类,类与类的代码之间要空两行,现在前2个属性的代码如下所示:
|
|
这段代码定义了房子的2个需要传入的属性,现在再定义剩余面积(free_area)和家具名称列表,如下所示:
|
|
此时,完成的任务包括:定义了一个家具类,定义了一个房子类。当输入了房子类型与面积这2个参数后,代码可以显示用房输入的这些内容,前面的这些代码运行的结果如下所示:
|
|
但是,还有2个任务没有完成,分别是①剩余面积还没有计算;②家具列表还是空的。下面完成这2个任务,思路是这个样子的:
- 我们需要判断一下家具的面积是否超过了剩余面积,如果超过,则提示不能添加这些家具;
- 将家具的名称追加到家具名称的列表中;
- 用房子的剩余面积减去家具面积。
现在我们往代码中补充一个添加家具(add_item)方法,这个方法的代码以及要实现的功能如下所示:
|
|
实现全部功能的完整代码如下所示:
|
|
运行结果如下所示:
|
|
如果我们把床的面积改为40,那么运行结果如下所示:
|
|
如果我们把这三个家具的总面积改为大于60(例如床面积为40,餐桌为20,衣柜为20),结果如下所示:
|
|
从添加家具这个案例我们可以知道这个案例中的面向对象思想:
- 主程序只负责创建房子对象和家具对象;
- 让房子对象调用add_item方法将家具添加到房子中;
- 面积计算、剩余面积、家具列表等处理都被封装到房子类的内部中。
案例分析四——士兵突击
在这里再次复习一下封装:
- 封装是面积对象编程的一个特点;;
- 面积对象编程的第一步就是将属性和方法封装到一个抽象的类中;
- 外界使用类创建对象(有的教程叫实例),然后让对象调用方法;
- 对象方法的细节都被封装到类的内部。
在这一小节中,还要学到一个知识点就是:一个对象的属性可以是另外一个类创建的对象。
案例需求
现在我们先看一下这个案例的需求:
- 士兵许三多有一把AK47
- 士兵可以开火;
- 枪能发射子弹;
- 枪装填子弹——增加子弹的数量。
从第一项需求中我们可以知道,我们要创建一个士兵类(Soldier)以及一个枪类(Gun),并且这个士兵类(Soldier)中含有枪类(Gun)这样一个属性,而这个属性则是由枪类(Gun)创建出来的一个对象。这就对应了我们前面提到的这个知识点,也就是说一个对象的属性可以是另外一个类创建的对象。
从第二项和第三项需求我们可以知道,士兵(Solider)对象中有一个开火(fire)的方法,而开火则是由枪发射子弹,那么还要在枪类(Gun)中创建一个发射的方法(shoot)。
从第四项需求可以知道,枪里面还应该有一个子弹数量这个属性,同时还要给枪创建一个装填子弹方法。
因此总结如下:
- 需要创建一个士兵类(Soldier),这个类中含有2属性,一个是士兵的名字(name),一个是枪(gun),同时还要定义一个方法,即开火(fire);
- 需要创建一个枪类(Gun),这个类中含有2个属性,一个是型号(model),即AK47,还有一个是子弹数量(count),同时还要定义2个方法,即装填子弹(add_bullet)与射击(shoot)这两个方法。
- 这里还有一个问题,是先定义枪类,还是士兵类,根据前面的知识,哪个类要被使用,就先定义哪个类。在这个案例中,是士兵使用枪,就先定义枪类。因为如果我们先定义士兵类,那么在士兵类的内部,还要用到枪的对象,此时枪类还没有被定义,就会比较麻烦。个人觉得,这个思路就是从小范围到大范围,从局部到整体。
以上两个类的示意图如下所示:
创建枪(Gun)类
在枪这个类中,型号(model)需要外界传递,而子弹数量(bullet_count)这个属性,我们假定开始的时候是没有子弹的,设为0,子弹需要人工装填,因此这个属性在初始阶段不需要外界输入,从上面的类图可以知道,枪里还有一个装填子弹方法(add_bullet)和发射子弹方法(shoot),因此枪类代码如下所示:
|
|
运行结果如下所示:
|
|
代码能够正常运行,到此,枪类(Gun)的定义已经完成。
现在设计士兵类(Soldier),这个类中含有2个属性,分别是姓名(name)和枪(gun),不过在这里,先假设每一个新兵都没有枪。在这里还要提示一下,如果不知道设置什么初始值,可以设置为None
。
None
关键字表示什么都没有;- 表示一个空对象,没有方法和属性,是一个特殊的常量;
- 可以将
None
赋值给任何一个变量。
现在分析一下士兵类中的fire
方法需求:
- 判断是否有枪,没有枪法没法冲锋;
- 减一声口号;
- 装填子弹;
- 射击。
完整代码如下所示:
|
|
运行结果如下所示:
|
|
在这个案例中,我们学到的内容就是:如果我们要实现某个任务,这个任务中有两个类,例如A类与B类,那么通过A类创建的对象中的属性可以是B类来源的对象,此时A创建的这个对象中的属性就是能调用B类中的方法。这个案例我觉得有点复杂,笔记不太可能记得很详细,可以多看几遍视频。
身份运算符
再来看一下前面代码中的某一句,即if self.gun == None
,选中后,会出现如下提示信息:
PyCharm的提示信息显示,如果与None
进行比较时,最好使用is
或is not
,而不是使用==
。这里的is
就是身份运算符。
身份运算符用于比较两个对象的内存地址是否一致,也就是说是否是对同一个对象的引用。在Python中,针对None
进行比较时,建议使用is
判断。Python中的身份运算符有2个,分别是is
与is not
,它们的功能如下所示:
运算符 | 描述 | 实例 |
---|---|---|
is | is是判断两个标识符是不是引用同一个对象 | x is y,类似id(x)==id(y) |
is not | is not是判断两个标识符是不是引用不同的对象 | x is not y,类似id(x)!=id(y) |
这里需要区分一下is
与==
的区别
is
用于判断两个变量引用对象是否为同一个;
==
用于判断引用变量的值是否相等,如下所示:
|
|
从上面的案例我们可以知道,a与b这两个列表的值相等,但引用不相等(也就是说这两个变量引用的内存地址不相同)。
而None
在Python中算是一个空对象,空对象不能把它理解为零,它是Python中的一个特殊的常量,指向内存中的一个地址,一个变量如果是None,它一定和None指向同一个内存地址。在上面的代码中,即if self.gun == None
这句中,==
后面接的是一个对象,因此最好使用is
,因为is
主要用于判断对象的引用,而不是值,因此Python建议使用is
来判断None。
网上又检索到了一些资料,如下所示:
在区分is
和==
这两种运算符区别之前,首先要知道Python中对象包含的三个基本要素,分别是:id
(身份标识)、type
(数据类型)和value
(值)。is
和==
都是对对象进行比较判断作用的,但对对象比较判断的内容并不相同。==
比较操作符和is
同一性运算符区别
==
是python标准操作符中的比较操作符,用来比较判断两个对象的value(值)是否相等,is
也被叫做同一性运算符,这个运算符比较判断的是对象间的唯一身份标识,也就是id是否相同。
私有属性和私有方法
应用场景及定义方式
应用场景
- 在实际开发中,对象的某些属性和方法可能只希望在对象的内部被使用,而不希望在外部被访问到;
- 私有属性就是对象不希望公开的属性;
- 私有方法就是对象不希望公开的方法。
定义方式
- 在定义属性和方法时,在属性名或者方法名前增加两个下划线,定义的就是私有属性或方法;
- 我们先看一个最常规的案例,如下所示:
|
|
运行结果如下所示:
|
|
在这个案例中,我们定义了一个女人类(Women),通过这个类创建了一个xiaofang对象,然后输出了这个对象的属性(age)与方法(secret)。
现在我们将age这个属性与方法改为私有属性,也就是在它们前面加两个下划线,变成__age
,如下所示:
|
|
运行结果如下所示:
|
|
运行结果出错,系统提示缺少属性__age
。现在我们将print(xiaofang.__age)
这句注释掉,如下所示:
|
|
运行结果如下所示:
|
|
能够正常运行,这说明在对象的secret
这个方法内部,可以访问这个对象的私有属性,也就是self.__age
。
现在我们把secret
这个方法也改为私有方法,即改为__secret
,如下所示:
|
|
结果运算如下所示:
|
|
结果也无法运行,提示没有__secret
这个方法。
伪私有属性和伪私有方法
在Python中并没有真正意义上的私有:
- 在给属性、方法命名时,实际是对名称做了一些特殊处理,使得外界无法访问到;
- 如果我们要强行访问这些智能属性与方法,处理方式就是在名称前面加上
__类名
=>_类名__名称
因此在Python中是有办法访问这些私有属性和私有方法,但在日常开发中,我们最好不要用这种方式来访问对象的私有属性或私有方法。
还来看一下前面的案例,如下所示:
|
|
运行结果如下所示:
|
|
解释器提示,没有__age
这个属性,现在我们改变一下代码,也就是将print(xiaofang.__age)
这句改为print(xiaofang._Women__age)
,改动的地方就是将私有属性前面添加上类名,并在类名前面再加一个下划线,完整代码如下所示:
|
|
运行结果如下所示:
|
|
通过这种方法,我们就能访问对象的私有属性。再来看一下__secret
这个私有方法的访问,也是同样的方法,即xiaofang._Women__secret
,如下所示:
|
|
运行结果如下所示:
|
|
我们现在也能访问这个私有方法,因此在Python中,没有绝对意义上的私有属性与私有方法,因此可以称为伪私有属性和伪私有方法
。
参考资料
- 黑马Python视频教程
- Python基础教程(第3版).Magnus Lie Hetland
- Python无师自通:专业程序员的养成