前言
这是黑马Python教程的笔记,内容是有关面向对象。
什么是类
类是一种对象,每个对象都属于特定的类,并被称为该类的实例。例如,如果你在窗外看到一只鸟,这只鸟就是“鸟类”的一个实例,鸟类是一个非常通用(抽象)的类,它有许多个子类:你看到的那只鸟可能属于子类”云雀“。你可以将“鸟类”视为由所有鸟组成的集合,而“云雀”是其一个子集,一个类的对象为另一个类的对象的子集时,前者就是后者的子类。因此“云雀”为“鸟类”的子类,而“鸟类”为“云雀”的超类。并且在面向对象的编程中,是先定义类,然后再由类生成一个实例。
通过上面的比喻,我们大致就理解了类,子类,超类。但在面向对象的编程中,子类可能还要更复些,因为类是由其支持的方法定义的。类的所有实例都有该类的所有方法,因此子类的所有实例都有超类的所有方法。因此,要定义子类,只需要定义多出来的方法(或者是重写某个方法)即可。例如鸟类(我们用Bird替代)可能提供方法fly,而Penguin类(Bird的一个子类)可能新增方法eat_fish。创建Penguin类时,我们还可能要重写超类方法,即方法fly,星Penguin不会飞,我们要在Penguin的实例中,方法fly应什么都不做或引发异常。
创建类
在使用面向对象开发之前,应该首先分析一下需要,确定程序中需要包括哪些类。在程度开发中,要设计一个类,通常需要满足以下三个要求:
- 类名。类名的命名要以驼峰式命名法进行命名,也就是每个单词的首字母要大写,例如
CapWords
; - 属性。属性赋予事物具有什么样的特性;
- 方法。方法赋予事物具有什么样的行为。
属性和方法的确定
描述对象特征的内容可以定义为属性
。对象具有的某些行为(动词),就可以定义为方法
。
例如我们看下面一个案例:
- 小明今年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
键,后面就会出现这个对象能使用的方法,如下所示:
In [3]: gl_list = []
In [4]: gl_list.
append() count() insert() reverse()
clear() extend() pop() sort()
copy() index() remove()
现在定义一个函数,如下所示:
In [6]: def demo():
...: """这是一个测试函数"""
...: print("Hello python")
...:
In [7]: demo()
Hello python
In [8]: demo.
此时输入demo后面再加一个点.
,没有任何信息,此时就需要用另外一种方法来验证它是一个对象,也就是使用dir
,如下所示:
In [10]: dir(demo)
Out[10]:
['__annotations__',
'__call__',
'__class__',
'__closure__',
'__code__',
'__defaults__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__get__',
'__getattribute__',
'__globals__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__kwdefaults__',
'__le__',
'__lt__',
'__module__',
'__name__',
'__ne__',
'__new__',
'__qualname__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__']
从上面的我们可以看出来,这里面是一个列表,它列出了很多方法,这些方法都是双下划线开头,双下划线结尾,这些东西都是Python内置的方法(__方法名__
),有些可以直接使用,例如__doc__
,如下所示:
In [11]: demo.__doc__
Out[11]: '这是一个测试函数'
此时我们就可以看到,使用__doc__
这个内置方法就能查看函数的文档说明,常用的一些内置方法/属性有以下这些:
序号 | 方法名 | 类型 | 作用 |
---|---|---|---|
1 | __new__ |
方法 | 创建对象时,会被自动调用 |
2 | __init__ |
方法 | 对象被初始化时,会被自动调用 |
3 | __del__ |
方法 | 对象被从内存中销毁之前,会被自动调用 |
4 | __str__ |
方法 | 返回对象的工描述信息,print函数输出使用 |
因此,dir()
这个函数很有用,例如当我想调用某个函数的方法时,一时想不想来,就可以使用dir()
这个函数。
定义简单的类
格式
在Python中要定义一个只包含方法的类,格式如下:
class 类名
def 方法1(self, 参数列表):
pass
def 方法2(self, 参数列表):
pass
创建对象
当一个类定义完全成,要使用这个类来创建对象,语法格式如下:
对象变量=类名()
创建对象
先看一个案例:例如小猫爱吃鱼,小猫要喝水。
分析:
- 定义一个猫类,Cat()
- 定义两个方法
eat
和drink
- 此时并没有涉及属性,因此我们不用定义属性。
class Cat:
def eat(self):
print("小猫爱吃鱼")
def drink(self):
print("小猫要喝水")
# 创建猫对象
tom = Cat()
tom.eat()
tom.drink()
运行结果如下所示:
小猫爱吃鱼
小猫要喝水
在这个案例中,我们先定义了一个猫类(Cat),然后创建了一个猫对象,再后,使这个对象赋予了吃与喝这两个方法,这两个方法已经封装到了类中,并不需要知道它的执行细节。
如果要用print
函数直接输出对象,那么输出结果就是此对象创建的内存地址,如下所示:
print(tom)
<__main__.Cat object at 0x0000017F72807A90>
创建多个对象
类只有一个,类只是一个模板,使用这个模板可以创建多个对象,如下所示:
class Cat:
def eat(self):
print("小猫爱吃鱼")
def drink(self):
print("小猫要喝水")
# 创建猫对象
tom = Cat()
tom.eat()
tom.drink()
# 再创建一个猫对象
lazy_cat = Cat()
lazy_cat.drink()
lazy_cat.eat()
print(tom)
print(lazy_cat)
class Cat:
def eat(self):
print("小猫爱吃鱼")
def drink(self):
print("小猫要喝水")
# 创建猫对象
tom = Cat()
# 再创建一个猫对象
lazy_cat = Cat()
print(tom)
print(lazy_cat)
我们来看一下结果:
<__main__.Cat object at 0x0000020AF8E47A90>
<__main__.Cat object at 0x0000020AF8E5F4E0>
从结果可以看出来,tom与lazy_cat这两个对象的内存地址不一样,它们是不一样的对象,再看一下以下代码:
lazy_cat2 = lazy_cat
print(lazy_cat2)
print(lazy_cat)
结果如下:
<__main__.Cat object at 0x000001C0424B7A90>
<__main__.Cat object at 0x000001C0424B7A90>
可以发现,lazy_cat和lazy_cat2这两个对象是一样的。
方法中的self参数
给对象添加属性
在Python中很容易给对象添加属性,但是这种做法并不推荐,因为对象的属性通常都已经封装到类中了,没必要再单独给对象添加属性,虽然不推荐,但还是要看一下案例,如下所示:
tom.name = "Tom"
输出对象属性
前面是找到,在类中定义的方法中的一个参数self
,这个self是指:哪一个对象调用的方法,self就是哪一个对象的引用。
例如代码中的tom = Cat()
,tom指向的对象就是由Cat这个类创建的,此时这个对象调用了Cat中的eat这个方法时,self指的也是这个对象,如果要想访问这个对象的属性,那么就是self加一个点(.
)即可,如下所示:
class Cat:
def eat(self):
print("%s 爱吃鱼"% self.name)#这一步我们可以使用self.name来访问对象的属性
def drink(self):
print(""%s 要喝水"% self.name)
# 创建猫对象
tom = Cat()
# 可能使用 .属性名 利用赋值语句就可以为对象添加属性
tom.name = "Tom"
tom.eat()
# 再创建一个猫对象
lazy_cat = Cat()
lazy_cat.name = "大懒猫"
lazy_cat.eat()
结果如下所示:
Tom 爱吃鱼
大懒猫 爱吃鱼
我们可以看到,原来的“小猫”就被替换成了“Tom”和“大懒猫”。也就是说,由哪一个对象调用的方法,方法内的self就是哪一个对象的引用。在类封装的方法内部,self就表示当前调用方法的对象自己。调用方法时,程序员不需要传递self参数。在方法内部,可以通过self.
访问对象的属性,也可以通过self.
调用其它的对象方法。
初始化方法
当使用类名()
创建对象时,会自动执行以下操作:
- 为对象在内存中分配空间——创建对象
- 为对象的属性设置初始值——初始化方法(init)
这个初始化方法就是__init__
方法,__init__
是对象的内置的方法,此方法是专门用来定义一个类具有哪些属性的方法,这个方法是固定的。我们在Cat
中增加__init__
方法,验证此方法在创建对象时会被自动调用,如下所示:
class Cat:
def __init__(self):
print("这是一个初始化方法")
tom = Cat()
运行结果如下所示:
这是一个初始化方法
从结果我们可以发现,使用类名()来创建对象的时候,会自动调用初始化方法__init__
。
在初始化方法内部定义属性
在__init__
方法内部使用self.属性名 = 属性的初始值
就可以定义属性
定义属性之后,再使用Cat类创建的对象都会拥有该属性,如下所示:
class Cat:
def __init__(self):
print("这是一个初始化方法")
# self.属性名 = 属性的初始值
self.name = "Tom"
tom = Cat()
print(tom.name)
运行结果,如下所示:
这是一个初始化方法
Tom
现在解释一下上面的代码,代码是从上到下执行的:
- 当Python解释器遇到
class Cat:
时,这段代码是不执行的,直接跳过去,跳到tom=Cat()
处; - 我们的代码运行到
tom=Cat()
处时,Python解释器此时会做两件事情:①在内存中为Cat对象分配一块空间,假设就是0x1234这块空间(16进制);②然后执行class Cat
这块的代码,类代码中的self
也会指向这块空间(即0x1234),此时也是从上到下执行的,执行到self.name = "Tom"
时,这块空间就被命名为了Tom,因此在使用print(tom.name)
时,就输出了空间的名字。
初始化方法的改造
在前面代码的基础上,我们再创建一个对象,lazy_cat
,如下所示:
class Cat:
def __init__(self):
print("这是一个初始化方法")
# self.属性名 = 属性的初始值
self.name = "Tom"
def eat(self):
print("%s爱吃鱼"%self.name)
tom = Cat()
print(tom.name)
lazy_cat = Cat()
lazy_cat.eat()
从上面代码我们可知,lazy_cat
这个对象的名称还是Tom
,如下所示:
这是一个初始化方法
Tom
这是一个初始化方法
Tom爱吃鱼
运行后,发现果然如此。因为我们在初始化时,已经把对象的名称固定了,就是self.name = "Tom"
这句代码。现在我们要解决这个问题。
如果要解决这个问题,我们需要在初始化方法中再定义一个形参,用于输入不同对象的名称,现在我们在def __init__(self)
中添加一个参数,new_name,即def __init__(self, new_name)
,当我们已经添加了这个形参后,在原来代码创建一个新对象时,也要添加相应的实参,例如tom = Cat()
就需要写为tom= Cat("Tom")
,完整代码如下所示:
class Cat:
def __init__(self, new_name):
print("这是一个初始化方法")
# self.属性名 = 属性的初始值
self.name = new_name
def eat(self):
print("%s爱吃鱼"%self.name)
tom = Cat("Tom")
print(tom.name)
lazy_cat = Cat("大懒猫")
lazy_cat.eat()
运行结果如下所示:
这是一个初始化方法
Tom
这是一个初始化方法
大懒猫爱吃鱼
此时我们就发现了,一个对象对应一个名称。
总结一下就是,在开发中,如果希望在创建对象的同时就设置对象的属性,就可以对__init__
方法进行改造:
- 把希望设置的属性值,定义成
__init__
方法的参数; - 在方法内部使用
self.属性 = 形参
接收外部 传递的参数; - 在创建对象时,使用
类名(属性1,属性2...)
调用。
内置方法和属性
这一部分介绍两个内置方法,如下所示:
序号 | 方法名 | 类型 | 作用 |
---|---|---|---|
01 | __del__ |
方法 | 对象被从内存中销毁之前,会被自动调用 |
02 | __str__ |
方法 | 返回对象的描述信息,print函数输出使用 |
__del__
方法
- 在Python中,当使用
类名()
创建对象时,为对象分配完空间后,自动调用__init__
方法。 - 当一个对象被从内存中销毁之前,会自动调用
__del__
方法。
应用场景
-
__init__
改造初始化方法,可以让创建对象更加灵活。 -
__del___
如果希望在对象被销毁之前,再做一些事情,可以使用__del__
方法。
生命周期
- 一个对象从调用
类名()
创建,生命周期开始。 - 一个对象的
__del__
方法一旦被调用,生命周期结束。 - 在对象的生命周期内,可以访问对象属性,或者让对象调用方法。
先来看一个案例,代码如下所示:
代码A:
class Cat:
def __init__(self, new_name):
self.name = new_name
print("%s 来了" % self.name)
def __del__(self):
print("%s 我去了"% self.name)
# tom是一个全局变量
tom = Cat("Tom")
print(tom.name)
# del tom
print("-"*50)
结果如下所示:
Tom 来了
Tom
--------------------------------------------------
Tom 我去了
现在,我们将代码中的del tom
显示出来,如下所示:
代码B:
class Cat:
def __init__(self, new_name):
self.name = new_name
print("%s 来了" % self.name)
def __del__(self):
print("%s 我去了"% self.name)
# tom是一个全局变量
tom = Cat("Tom")
print(tom.name)
del tom
print("-"*50)
运行结果如下所示:
Tom 来了
Tom
Tom 我去了
--------------------------------------------------
从结果中我们可以发现,代码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__
方法必须返回一个字符。
看下面的案例,在这个案例,我们直接输出一个对象(有的教程在此处称之为“实例”),如下所示:
class Cat:
def __init__(self, new_name):
self.name = new_name
print("%s 来了" % self.name)
def __del__(self):
print("%s 我去了"% self.name)
# tom是一个全局变量
tom = Cat("Tom")
print(tom)
运行结果如下所示:
Tom 来了
<__main__.Cat object at 0x0000021E1311B278>
Tom 我去了
其中第二行,即<__main__.Cat object at 0x0000021E1311B278>
这里输出的是Cat这个类,并把tom这个对象在内存中的地址显示出来。现在我们在类中增加__str___
这个方法,如下所示:
class Cat:
def __init__(self, new_name):
self.name = new_name
print("%s 来了" % self.name)
def __del__(self):
print("%s 我去了"% self.name)
def __str__(self):
return "我是小猫"
# tom是一个全局变量
tom = Cat("Tom")
print(tom)
运行结果如下所示:
Tom 来了
我是小猫
Tom 我去了
此时,我们改造了__str__
这个方法后,输出的就不再是这个对象的类,以及这个对象的地址了。而是我们自定义的内容,现在把__str__
中的方法再改造一下,改成return "我是小猫[%s]"% self.name
,则结果就如下所示:
Tom 来了
我是小猫[Tom]
Tom 我去了
面向对象封装案例
封装
- 封装是面向对象编程的一大特点;
- 面向对象编程的第一步就是将属性和方法封装到一个抽象的类中;
- 外界使用类创建对象(有的教程叫实例),然后让对象调用方法;
- 对象方法中的细节都被封装在类的内部。
案例分析——小明爱跑步
我们以“小明爱跑步”为例说明一下,先分析需求,如下所示:
- 小明体重75.0公斤;
- 小明每次跑步会减肥0.5公斤;
- 小明每次吃东西体重增加1公斤。
代码分析
代码如下所示:
class Person:
def __init__(self, name, weight):
self.name = name
self.weight = weight
def __str__(self):
return "我的名字叫 %s 体重是 %.2f 公斤" %(self.name, self.weight)
def run(self):
print("%s 爱跑步, 跑步锻炼身体"%self.name)
self.weight -= 0.5
def eat(self):
print("%s 是吃货,吃完这顿再减肥"%self.name)
self.weight += 1
xiaoming = Person("小明", 75.0)
xiaoming.run()
xiaoming.eat()
print(xiaoming)
运行结果如下所示:
小明 爱跑步, 跑步锻炼身体
小明 是吃货,吃完这顿再减肥
我的名字叫 小明 体重是 75.50 公斤
如果我们把Person
这个类折叠起来,就是下面的这个样子,如下所示:
class Person:...
xiaoming = Person("小明", 75.0)
xiaoming.run()
xiaoming.eat()
print(xiaoming)
从上面折叠后的代码我们就知道,我们创建了一个Person
这个类,再由这个类创建了一个叫xiaoming
的对象,然后这个对象进行了跑步(run
)和吃东西(eat
),具体这跑步与吃东西它们的代码,都已经封装到了Person
这个类中,我们直接调用即可。
案例扩展——小美也爱跑步
在原来案例的基础上进行扩展,先看一下需要:
- 小明和小美都爱跑步;
- 小明体重75.0公斤;
- 小美体重45.0公斤;
- 每次跑步都会减少0.5公斤;
- 每次吃东西都会增加1公斤。
代码分析——案例扩展
再使用Persion
这个类创建一个新对象,即xiaomei
,如下所示:
class Person:
def __init__(self, name, weight):
self.name = name
self.weight = weight
def __str__(self):
return "我的名字叫 %s 体重是 %.2f 公斤" %(self.name, self.weight)
def run(self):
print("%s 爱跑步, 跑步锻炼身体"%self.name)
self.weight -= 0.5
def eat(self):
print("%s 是吃货,吃完这顿再减肥"%self.name)
self.weight += 1
xiaoming = Person("小明", 75.0)
xiaoming.run()
xiaoming.eat()
print(xiaoming)
# 小美爱跑步
xiaomei = Person("小美", 45)
xiaomei.eat()
xiaomei.run()
print(xiaomei)
print(xiaoming)
结果如下所示:
小明 爱跑步, 跑步锻炼身体
小明 是吃货,吃完这顿再减肥
我的名字叫 小明 体重是 75.50 公斤
小美 是吃货,吃完这顿再减肥
小美 爱跑步, 跑步锻炼身体
我的名字叫 小美 体重是 45.50 公斤
我的名字叫 小明 体重是 75.50 公斤
我们可以看到,由一个类创建的两个对象。在每个对象的方法内部,可以直接访问对象的属性。每个对象各自使用各自的方法,属性互不影响。
案例分析三——摆放家具
现在我们再看一个案例,摆放家具,看一下需求:
- 房子(House)有户型、总面积和家具名称列表,而新房子没有任何家具;
- 家具(HouseItem)有名字和占地面积,其中不同的家具占地面积也不一样,例如床(bed)占地4平方米,衣柜(chest)占地2平方米,餐桌(table)占地1.5平方米;
- 现在我们要将2中的3样家具添加到房子中;
- 输出房子时,要求输出这些信息:户型、总面积、剩余面积和家具名称列表。
有了上面需求后,我们要考虑一下如何设计代码:
- 定义2个类,一个是房子(House),一个是家具(HouseItem)。
- 房子有4个属性;
- 家具有2个属性。
但是,房子与家具这两个类先定义哪个?思路就是,由于房子这个类中要用到家具(家具有面积,房子的剩余面积与家具有关),因此我们要先定义家具这个类,总之就是,被使用的类要先定义。
定义家具类
根据前面的分析思路,现在先定义一个家具类,如下所示:
class HouseItem():
def __init__(self, name, area):
self.name = name
self.area = area
def __str__(self):
return "[%s] 占地 %.2f"%(self.name, self.area)
# 创建家具
bed = HouseItem("床", 4)
chest = HouseItem("衣柜", 2)
table = HouseItem("餐桌", 1.5)
print(bed)
print(chest)
print(table)
结果运行如下所示:
[床] 占地 4.00
[衣柜] 占地 2.00
[餐桌] 占地 1.50
定义房子类
思路:房子中需要定义4个属性,分别为房子类型(house_type),面积(area),剩余面积(free_area),家具列表(item_list)。但是,在给房子传递参数时,只需要传递其中的2个即可,分别是房子类型(house_type)与面积(area),因为剩余面积(free_area)可以由这2个参数算出来,而家具列表(item_list)在初始情况下是一个空列表,,也不需要传入。
注:在同一个代码文件中,如果要定义2个以及2个以上的类,类与类的代码之间要空两行,现在前2个属性的代码如下所示:
class House:
def __init__(self, house_type, area):
self.house_type = house_type
self.area = area
这段代码定义了房子的2个需要传入的属性,现在再定义剩余面积(free_area)和家具名称列表,如下所示:
class House:
def __init__(self, house_type, area):
self.house_type = house_type
self.area = area
#剩余面积
self.free_area = area
#家具名称
self.item_list = []
# 刚开始的时候,就是一个空列表
def __str__(self):
# 定义描述方法
# 这里显示的是return返回的内容,如下所示:
return ("户型:%s \n总面积: %.2f[剩余: %.2f]\n家具: %s"
% (self.house_type,
self.area,
self.free_area,
self.item_list))
# 创建房子对象
my_house = House("两室一厅", 60)
my_house.add_item(bed)
# 添加一张床
my_house.add_item(chest)
# 添加一个衣柜
my_house.add_item(table)
# 添加一张餐桌
此时,完成的任务包括:定义了一个家具类,定义了一个房子类。当输入了房子类型与面积这2个参数后,代码可以显示用房输入的这些内容,前面的这些代码运行的结果如下所示:
[床] 占地 4.00
[衣柜] 占地 2.00
[餐桌] 占地 1.50
要添加 [床] 占地 4.00
要添加 [衣柜] 占地 2.00
要添加 [餐桌] 占地 1.50
户型:两室一厅
总面积: 60.00[剩余: 60.00]
家具: []
但是,还有2个任务没有完成,分别是①剩余面积还没有计算;②家具列表还是空的。下面完成这2个任务,思路是这个样子的:
- 我们需要判断一下家具的面积是否超过了剩余面积,如果超过,则提示不能添加这些家具;
- 将家具的名称追加到家具名称的列表中;
- 用房子的剩余面积减去家具面积。
现在我们往代码中补充一个添加家具(add_item)方法,这个方法的代码以及要实现的功能如下所示:
def add_item(self, item):
print("要添加 %s" % item)
# 1. 判断家具的面积
if item.area > self.free_area:
print("%s 的面积太大了,无法添加"% item.name)
return
# 2. 将家具的名称添加到列表中
self.item_list.append(item.name)
# 3. 计算剩余面积
self.free_area -= item.area
实现全部功能的完整代码如下所示:
class HouseItem():
def __init__(self, name, area):
self.name = name
self.area = area
def __str__(self):
return "[%s] 占地 %.2f"%(self.name, self.area)
class House:
def __init__(self, house_type, area):
self.house_type = house_type
self.area = area
#剩余面积
self.free_area = area
#家具名称
self.item_list = []
# 刚开始的时候,就是一个空列表
def __str__(self):
# 这里显示的是return返回的内容,如下所示:
return ("户型:%s \n总面积: %.2f[剩余: %.2f]\n家具: %s"
% (self.house_type,
self.area,
self.free_area,
self.item_list))
def add_item(self, item):
print("要添加 %s" % item)
# 1. 判断家具的面积
if item.area > self.free_area:
print("%s 的面积太大了,无法添加"% item.name)
return
# 2. 将家具的名称添加到列表中
self.item_list.append(item.name)
# 3. 计算剩余面积
self.free_area -= item.area
# 创建家具
bed = HouseItem("床", 4)
chest = HouseItem("衣柜", 2)
table = HouseItem("餐桌", 1.5)
print(bed)
print(chest)
print(table)
# # 创建房子对象
my_house = House("两室一厅", 60)
#
my_house.add_item(bed)
# # 添加一张床
#
# my_house.add_item(chest)
# # 添加一个衣柜
#
# my_house.add_item(table)
# # 添加一张餐桌
print(my_house)
运行结果如下所示:
[床] 占地 4.00
[衣柜] 占地 2.00
[餐桌] 占地 1.50
要添加 [床] 占地 4.00
户型:两室一厅
总面积: 60.00[剩余: 56.00]
家具: ['床']
如果我们把床的面积改为40,那么运行结果如下所示:
[床] 占地 40.00
[衣柜] 占地 2.00
[餐桌] 占地 1.50
要添加 [床] 占地 40.00
户型:两室一厅
总面积: 60.00[剩余: 20.00]
家具: ['床']
如果我们把这三个家具的总面积改为大于60(例如床面积为40,餐桌为20,衣柜为20),结果如下所示:
[床] 占地 40.00
[衣柜] 占地 20.00
[餐桌] 占地 20.00
要添加 [床] 占地 40.00
要添加 [衣柜] 占地 20.00
要添加 [餐桌] 占地 20.00
餐桌 的面积太大了,无法添加
户型:两室一厅
总面积: 60.00[剩余: 0.00]
家具: ['床', '衣柜']
从添加家具这个案例我们可以知道这个案例中的面向对象思想:
- 主程序只负责创建房子对象和家具对象;
- 让房子对象调用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),因此枪类代码如下所示:
class Gun:
def __init__(self, model):
# 1. 枪的型号
self.model = model
# 2. 子弹的数量
self.bullet_count = 0
def add_bullet(self, count):
self.bullet_count += count
def shoot(self):
# 1. 判断子弹数量
if self.bullet_count <= 0:
print("[%s] 没有子弹了..." % self.model)
return
# 2. 发射子弹
self.bullet_count -= 1
# 3. 提示发射信息
print("[%s] 突突突...[%d]" % (self.model, self.bullet_count))
ak47 = Gun("AK47")
ak47.add_bullet(50)
ak47.shoot()
运行结果如下所示:
[AK47] 突突突...[49]
代码能够正常运行,到此,枪类(Gun)的定义已经完成。
现在设计士兵类(Soldier),这个类中含有2个属性,分别是姓名(name)和枪(gun),不过在这里,先假设每一个新兵都没有枪。在这里还要提示一下,如果不知道设置什么初始值,可以设置为None
。
-
None
关键字表示什么都没有; - 表示一个空对象,没有方法和属性,是一个特殊的常量;
- 可以将
None
赋值给任何一个变量。
现在分析一下士兵类中的fire
方法需求:
- 判断是否有枪,没有枪法没法冲锋;
- 减一声口号;
- 装填子弹;
- 射击。
完整代码如下所示:
class Gun:
def __init__(self, model):
# 1. 枪的型号
self.model = model
# 2. 子弹的数量
self.bullet_count = 0
def add_bullet(self, count):
self.bullet_count += count
def shoot(self):
# 1. 判断子弹数量
if self.bullet_count <= 0:
print("[%s] 没有子弹了..." % self.model)
return
# 2. 发射子弹
self.bullet_count -= 1
# 3. 提示发射信息
print("[%s] 突突突...[%d]" % (self.model, self.bullet_count))
class Soldier:
def __init__(self, name):
# 1. 姓名
self.name = name
# 2. 枪 - 新兵没有枪
self.gun = None
def fire(self):
# 1. 判断士兵是否有枪
if self.gun == None:
print("[%s] 还没有枪..."% self.name)
return
# 2. 高喊口号
print("冲啊...[%s]"% self.name)
# 3. 让枪装填子弹
self.gun.add_bullet(50)
# 4. 发射子弹
self.gun.shoot()
ak47 = Gun("AK47")
# 2. 创建许三多
xusanduo = Soldier("许三多")
xusanduo.gun = ak47
xusanduo.fire()
print(xusanduo.gun)
运行结果如下所示:
冲啊...[许三多]
[AK47] 突突突...[49]
<__main__.Gun object at 0x000002E8B48D9668>
在这个案例中,我们学到的内容就是:如果我们要实现某个任务,这个任务中有两个类,例如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
用于判断两个变量引用对象是否为同一个;
==
用于判断引用变量的值是否相等,如下所示:
In [1]: a = [1, 2, 3]
In [2]: id(a)
Out[2]: 2681339468296
In [3]: b = [1, 2, 3]
In [4]: id(b)
Out[4]: 2681339465864
In [5]: a == b
Out[5]: True
In [6]: a is b
Out[6]: False
从上面的案例我们可以知道,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是否相同。
私有属性和私有方法
应用场景及定义方式
应用场景
- 在实际开发中,对象的某些属性和方法可能只希望在对象的内部被使用,而不希望在外部被访问到;
- 私有属性就是对象不希望公开的属性;
- 私有方法就是对象不希望公开的方法。
定义方式
- 在定义属性和方法时,在属性名或者方法名前增加两个下划线,定义的就是私有属性或方法;
- 我们先看一个最常规的案例,如下所示:
class Women:
def __init__(self, name):
self.name = name
self.age = 18
def secret(self):
print("%s 的年龄是%d"%(self.name, self.age))
xiaofang = Women("小芳")
print(xiaofang.age)
xiaofang.secret()
运行结果如下所示:
18
小芳 的年龄是18
在这个案例中,我们定义了一个女人类(Women),通过这个类创建了一个xiaofang对象,然后输出了这个对象的属性(age)与方法(secret)。
现在我们将age这个属性与方法改为私有属性,也就是在它们前面加两个下划线,变成__age
,如下所示:
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
# 私有属性在外界不能被直接访问
print(xiaofang.__age)
xiaofang.secret()
运行结果如下所示:
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/面向对象/hm_17_私有属性和方法.py", line 13, in <module>
print(xiaofang.__age)
AttributeError: 'Women' object has no attribute '__age'
运行结果出错,系统提示缺少属性__age
。现在我们将print(xiaofang.__age)
这句注释掉,如下所示:
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def secret(self):
# 在对象的方法内部,是可以访问对象的私有属性的
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
# 私有属性在外界不能被直接访问
# print(xiaofang.__age)
xiaofang.secret()
运行结果如下所示:
小芳 的年龄是18
能够正常运行,这说明在对象的secret
这个方法内部,可以访问这个对象的私有属性,也就是self.__age
。
现在我们把secret
这个方法也改为私有方法,即改为__secret
,如下所示:
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def __secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
# print(xiaofang.__age)
xiaofang.__secret()
结果运算如下所示:
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/面向对象/hm_17_私有属性和方法.py", line 15, in <module>
xiaofang.__secret()
AttributeError: 'Women' object has no attribute '__secret'
结果也无法运行,提示没有__secret
这个方法。
伪私有属性和伪私有方法
在Python中并没有真正意义上的私有:
- 在给属性、方法命名时,实际是对名称做了一些特殊处理,使得外界无法访问到;
- 如果我们要强行访问这些智能属性与方法,处理方式就是在名称前面加上
__类名
=>_类名__名称
因此在Python中是有办法访问这些私有属性和私有方法,但在日常开发中,我们最好不要用这种方式来访问对象的私有属性或私有方法。
还来看一下前面的案例,如下所示:
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def __secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
print(xiaofang.__age)
# xiaofang.__secret()
运行结果如下所示:
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/面向对象/hm_18_伪私有属性和方法.py", line 13, in <module>
print(xiaofang.__age)
AttributeError: 'Women' object has no attribute '__age'
解释器提示,没有__age
这个属性,现在我们改变一下代码,也就是将print(xiaofang.__age)
这句改为print(xiaofang._Women__age)
,改动的地方就是将私有属性前面添加上类名,并在类名前面再加一个下划线,完整代码如下所示:
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def __secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
print(xiaofang._Women__age)
# xiaofang.__secret()
运行结果如下所示:
18
通过这种方法,我们就能访问对象的私有属性。再来看一下__secret
这个私有方法的访问,也是同样的方法,即xiaofang._Women__secret
,如下所示:
class Women:
def __init__(self, name):
self.name = name
self.__age = 18
def __secret(self):
print("%s 的年龄是%d"%(self.name, self.__age))
xiaofang = Women("小芳")
print(xiaofang._Women__age)
xiaofang._Women__secret()
运行结果如下所示:
18
小芳 的年龄是18
我们现在也能访问这个私有方法,因此在Python中,没有绝对意义上的私有属性与私有方法,因此可以称为伪私有属性和伪私有方法
。