面向对象
四、面向对象封装案例
1. 封装
- 封装 是面向对象编程的一大特点
- 面向对象编程的 第一步 —— 将 属性 和 方法 封装 到一个抽象的 类 中
- 外界 使用 类 创建 对象,然后 让对象调用方法
- 对象方法的细节 都被 封装 在 类的内部
2. 小明爱跑步
-
需求
- 小明 体重 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)
xiaoming.run()
xiaoming.eat()
xiaoming.eat()
print(xiaoming)
3. 摆放家具
- 需求
- 房子(House) 有 户型、总面积 和 家具名称列表
- 新房子没有任何的家具
- 家具(HouseItem) 有 名字 和 占地面积,其中
- 席梦思(bed) 占地 4 平米
- 衣柜(chest) 占地 2 平米
- 餐桌(table) 占地 1.5 平米
- 将以上三件 家具 添加 到 房子 中
- 打印房子时,要求输出:户型、总面积、剩余面积、家具名称列表
- 房子(House) 有 户型、总面积 和 家具名称列表
- 剩余面积
- 在创建房子对象时,定义一个 剩余面积的属性,初始值和总面积相等
- 当调用 add_item 方法,向房间 添加家具 时,让 剩余面积 -= 家具面积
-
思考:应该先开发哪一个类?
-
答案 —— 家具类
- 家具简单
- 房子要使用到家具,被使用的类,通常应该先开发
3.1 创建家具
class HouseItem:
def __init__(self, name, area):
"""
:param name: 家具名称
:param area: 占地面积
"""
self.name = name
self.area = area
def __str__(self):
return "[%s] 占地面积 %.2f" % (self.name, self.area)
# 1. 创建家具
bed = HouseItem("席梦思", 4)
chest = HouseItem("衣柜", 2)
table = HouseItem("餐桌", 1.5)
print(bed)
print(chest)
print(table)
- 小结
- 创建了一个 家具类,使用到 init 和 str 两个内置方法
- 使用 家具类 创建了 三个家具对象,并且 输出家具信息
3.2 创建房间
class House:
def __init__(self, house_type, area):
"""
:param house_type: 户型
:param area: 总面积
"""
self.house_type = house_type
self.area = area
# 剩余面积默认和总面积一致
self.free_area = area
# 默认没有任何的家具
self.item_list = []
def __str__(self):
# Python 能够自动的将一对括号内部的代码连接在一起
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)
...
# 2. 创建房子对象
my_home = House("两室一厅", 60)
my_home.add_item(bed)
my_home.add_item(chest)
my_home.add_item(table)
print(my_home)
- 小结
- 创建了一个 房子类,使用到 __init__ 和 __str__ 两个内置方法
- 准备了一个 add_item 方法 准备添加家具
- 使用 房子类 创建了 一个房子对象
- 让 房子对象 调用了三次 add_item 方法,将 三件家具 以实参传递到 add_item 内部
3.3 添加家具
- 需求
- 1> 判断 家具的面积 是否 超过剩余面积,如果超过,提示不能添加这件家具
- 2> 将 家具的名称 追加到 家具名称列表 中
- 3> 用 房子的剩余面积 - 家具面积
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
3.4 小结
- 主程序只负责创建 房子 对象和 家具 对象
- 让 房子 对象调用 add_item 方法 将家具添加到房子中
- 面积计算、剩余面积、家具列表 等处理都被 封装 到 房子类的内部
五、面向对象封装案例 II
1. 士兵突击
- 需求
- 士兵 许三多 有一把 AK47
- 士兵 可以 开火
- 枪 能够 发射 子弹
- 枪 装填 装填子弹 —— 增加子弹数量
1.1 开发枪类
- shoot 方法需求
- 1> 判断是否有子弹,没有子弹无法射击
- 2> 使用 print 提示射击,并且输出子弹数量
class Gun:
def __init__(self, model):
# 枪的型号
self.model = model
# 子弹数量
self.bullet_count = 0
def add_bullet(self, count):
self.bullet_count += count
def shoot(self):
# 判断是否还有子弹
if self.bullet_count <= 0:
print("没有子弹了...")
return
# 发射一颗子弹
self.bullet_count -= 1
print("%s 发射子弹[%d]..." % (self.model, self.bullet_count))
# 创建枪对象
ak47 = Gun("ak47")
ak47.add_bullet(50)
ak47.shoot()
1.2 开发士兵类
- 假设:每一个新兵 都 没有枪
- 定义没有初始值的属性
- 在定义属性时,如果 不知道设置什么初始值,可以设置为 None
- None 关键字 表示 什么都没有
- 表示一个 空对象,没有方法和属性,是一个特殊的常量
- 可以将 None 赋值给任何一个变量
- fire 方法需求
- 1> 判断是否有枪,没有枪没法冲锋
- 2> 喊一声口号
- 3> 装填子弹
- 4> 射击
class Soldier:
def __init__(self, name):
# 姓名
self.name = name
# 枪,士兵初始没有枪 None 关键字表示什么都没有
self.gun = None
def fire(self):
# 1. 判断士兵是否有枪
if self.gun is None:
print("[%s] 还没有枪..." % self.name)
return
# 2. 高喊口号
print("冲啊...[%s]" % self.name)
# 3. 让枪装填子弹
self.gun.add_bullet(50)
# 4. 让枪发射子弹
self.gun.shoot()
- 小结
- 创建了一个 士兵类,使用到 __init__ 内置方法
- 在定义属性时,如果 不知道设置什么初始值,可以设置为 None
- 在 封装的 方法内部,还可以让 自己的 使用其他类创建的对象属性 调用已经 封装好的方法
2. 身份运算符
- 身份运算符用于 比较 两个对象的 内存地址 是否一致 —— 是否是对同一个对象的引用
- 在 Python 中针对 None 比较时,建议使用 is 判断
运算符 | 描述 | 实例 |
---|---|---|
is | is 是判断两个标识符是不是引用同一个对象 | x is y,类似 id(x) == id(y) |
is not | is not 是判断两个标识符是不是引用不同对象 | x is not y,类似 id(a) != id(b) |
- is 与 == 区别:
- is 用于判断 两个变量 引用对象是否为同一个
- == 用于判断 引用变量的值 是否相等
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> b is a
False
>>> b == a
True
六、私有属性和私有方法
1. 应用场景及定义方式
- 应用场景
- 在实际开发中,对象 的 某些属性或方法 可能只希望 在对象的内部被使用,而 不希望在外部被访问到
- 私有属性 就是 对象 不希望公开的 属性
- 私有方法 就是 对象 不希望公开的 方法
- 定义方式
- 在 定义属性或方法时,在 属性名或者方法名前 增加 两个下划线,定义的就是 私有 属性或方法
- 在 定义属性或方法时,在 属性名或者方法名前 增加 两个下划线,定义的就是 私有 属性或方法
class Women:
def __init__(self, name):
self.name = name
# 不要问女生的年龄
self.__age = 18
def __secret(self):
print("我的年龄是 %d" % self.__age)
xiaofang = Women("小芳")
# 私有属性,外部不能直接访问
# print(xiaofang.__age)
# 私有方法,外部不能直接调用
# xiaofang.__secret()
2. 伪私有属性和私有方法(科普)
-
提示:在日常开发中,不要使用这种方式,访问对象的 私有属性 或 私有方法
-
Python 中,并没有 真正意义 的 私有
- 在给 属性、方法 命名时,实际是对 名称 做了一些特殊处理,使得外界无法访问到
- 处理方式:在 名称 前面加上 _类名 => _类名__名称
print(xiaofang._Women__age) # 18
xiaofang._Women__secret() # 我的年龄是 18
七、单例
1. 单例设计模式
-
设计模式
- 设计模式 是 前人工作的总结和提炼,通常,被人们广泛流传的设计模式都是针对 某一特定问题 的成熟的解决方案
- 使用 设计模式 是为了可重用代码、让代码更容易被他人理解、保证代码可靠性
-
单例设计模式
- 目的 —— 让 类 创建的对象,在系统中 只有 唯一的一个实例
- 每一次执行 类名() 返回的对象,内存地址是相同的
-
单例设计模式的应用场景
- 音乐播放 对象
- 回收站 对象
- 打印机 对象
2. __new__ 方法
- 使用 类名() 创建对象时,Python 的解释器 首先 会 调用 __new__ 方法为对象 分配空间
- __new__ 是一个 由 object 基类提供的 内置的静态方法,主要作用有两个:
○
ⅰ. 在内存中为对象 分配空间
○
ⅱ. 返回 对象的引用 - Python 的解释器获得对象的 引用 后,将引用作为 第一个参数,传递给\ _init_ 方法
- 重写 new 方法 的代码非常固定!
- 重写 new 方法 一定要 return super().__new__(cls)
- 否则 Python 的解释器 得不到 分配了空间的 对象引用,就不会调用对象的初始化方法
- 注意:new 是一个静态方法,在调用时需要 主动传递 cls 参数
class MusicPlayer(object):
def __new__(cls, *args, **kwargs):
# 如果不返回任何结果,
return super().__new__(cls)
def __init__(self):
print("初始化音乐播放对象")
player = MusicPlayer()
print(player)
3. Python 中的单例
- 单例 —— 让 类 创建的对象,在系统中 只有 唯一的一个实例
a. 定义一个 类属性,初始值是 None,用于记录 单例对象的引用
b. 重写 new 方法
c. 如果 类属性 is None,调用父类方法分配空间,并在类属性中记录结果
d. 返回 类属性 中记录的 对象引用
class MusicPlayer(object):
# 定义类属性记录单例对象引用
instance = None
def __new__(cls, *args, **kwargs):
# 1. 判断类属性是否已经被赋值
if cls.instance is None:
cls.instance = super().__new__(cls)
# 2. 返回类属性的单例引用
return cls.instance
- 只执行一次初始化工作
- 在每次使用 类名() 创建对象时,Python 的解释器都会自动调用两个方法:
- __new__ 分配空间
- __init__ 对象初始化
- 在上一小节对 __new__ 方法改造之后,每次都会得到 第一次被创建对象的引用
- 但是:初始化方法还会被再次调用
- 在每次使用 类名() 创建对象时,Python 的解释器都会自动调用两个方法:
- 需求
- 让 初始化动作 只被 执行一次
- 解决办法
- 定义一个类属性 init_flag 标记是否 执行过初始化动作,初始值为 False
- 在 init 方法中,判断 init_flag,如果为 False 就执行初始化动作
- 然后将 init_flag 设置为 True
- 这样,再次 自动 调用 init 方法时,初始化动作就不会被再次执行 了
class MusicPlayer(object):
# 记录第一个被创建对象的引用
instance = None
# 记录是否执行过初始化动作
init_flag = False
def __new__(cls, *args, **kwargs):
# 1. 判断类属性是否是空对象
if cls.instance is None:
# 2. 调用父类的方法,为第一个对象分配空间
cls.instance = super().__new__(cls)
# 3. 返回类属性保存的对象引用
return cls.instance
def __init__(self):
if not MusicPlayer.init_flag:
print("初始化音乐播放器")
MusicPlayer.init_flag = True
# 创建多个对象
player1 = MusicPlayer()
print(player1)
player2 = MusicPlayer()
print(player2)
八、多态
1. 面向对象三大特性
- 封装 根据 职责 将 属性 和 方法 封装 到一个抽象的 类 中
○ 定义类的准则 - 继承 实现代码的重用,相同的代码不需要重复的编写
○ 设计类的技巧
○ 子类针对自己特有的需求,编写特定的代码 - 多态 不同的 子类对象 调用相同的 父类方法,产生不同的执行结果
○ 多态 可以 增加代码的灵活度
○ 以 继承 和 重写父类方法 为前提
○ 是调用方法的技巧,不会影响到类的内部设计
2. 多态案例演练
- 需求
- 在 Dog 类中封装方法 game
○ 普通狗只是简单的玩耍 - 定义 XiaoTianDog 继承自 Dog,并且重写 game 方法
○ 哮天犬需要在天上玩耍 - 定义 Person 类,并且封装一个 和狗玩 的方法
○ 在方法内部,直接让 狗对象 调用 game 方法
- 案例小结
- Person 类中只需要让 狗对象 调用 game 方法,而不关心具体是 什么狗
- game 方法是在 Dog 父类中定义的
- 在程序执行时,传入不同的 狗对象 实参,就会产生不同的执行效果
- Person 类中只需要让 狗对象 调用 game 方法,而不关心具体是 什么狗
- 多态 更容易编写出出通用的代码,做出通用的编程,以适应需求的不断变化!
class Dog(object):
def __init__(self, name):
self.name = name
def game(self):
print("%s 蹦蹦跳跳的玩耍..." % self.name)
class XiaoTianDog(Dog):
def game(self):
print("%s 飞到天上去玩耍..." % self.name)
class Person(object):
def __init__(self, name):
self.name = name
def game_with_dog(self, dog):
print("%s 和 %s 快乐的玩耍..." % (self.name, dog.name))
# 让狗玩耍
dog.game()
# 1. 创建一个狗对象
# wangcai = Dog("旺财")
wangcai = XiaoTianDog("飞天旺财")
# 2. 创建一个小明对象
xiaoming = Person("小明")
# 3. 让小明调用和狗玩的方法
xiaoming.game_with_dog(wangcai)
九、继承
1. 单继承
1.1 继承的概念、语法和特点
-
继承的概念:子类 拥有 父类 的所有 方法 和 属性
-
继承的语法
class 类名(父类名): pass
- 子类 继承自 父类,可以直接 享受 父类中已经封装好的方法,不需要再次开发
- 子类 中应该根据 职责,封装 子类特有的 属性和方法
-
专业术语
- Dog 类是 Animal 类的子类,Animal 类是 Dog 类的父类,Dog 类从 Animal 类继承
- Dog 类是 Animal 类的派生类,Animal 类是 Dog 类的基类,Dog 类从 Animal 类派生
-
继承的传递性
- C 类从 B 类继承,B 类又从 A 类继承
- 那么 C 类就具有 B 类和 A 类的所有属性和方法
-
子类 拥有 父类 以及 父类的父类 中封装的所有 属性 和 方法
-
提问:哮天犬 能够调用 Cat 类中定义的 catch 方法吗?
-
答案:不能,因为 哮天犬 和 Cat 之间没有 继承 关系
1.2 方法的重写
-
子类 拥有 父类 的所有 方法 和 属性
-
子类 继承自 父类,可以直接 享受 父类中已经封装好的方法,不需要再次开发
-
应用场景
- 当 父类 的方法实现不能满足子类需求时,可以对方法进行 重写(override)
- 当 父类 的方法实现不能满足子类需求时,可以对方法进行 重写(override)
-
重写 父类方法有两种情况:
- 覆盖 父类的方法
- 对父类方法进行 扩展
-
覆盖父类的方法
-
如果在开发中,父类的方法实现 和 子类的方法实现,完全不同
-
就可以使用 覆盖 的方式,在子类中 重新编写 父类的方法实现
-
具体的实现方式,就相当于在 子类中 定义了一个 和父类同名的方法并且实现
-
重写之后,在运行时,只会调用 子类中重写的方法,而不再会调用 父类封装的方法
-
-
对父类方法进行 扩展
- 如果在开发中,子类的方法实现 中 包含 父类的方法实现
- 父类原本封装的方法实现 是 子类方法的一部分
-
就可以使用 扩展 的方式
a. 在子类中 重写 父类的方法
b. 在需要的位置使用 super().父类方法 来调用父类方法的执行
c. 代码其他的位置针对子类的需求,编写 子类特有的代码实现 -
关于 super
- 在 Python 中 super 是一个 特殊的类
- super() 就是使用 super 类创建出来的对象
- 最常 使用的场景就是在 重写父类方法时,调用 在父类中封装的方法实现
-
调用父类方法的另外一种方式(知道)
- 在 Python 2.x 时,如果需要调用父类的方法,还可以使用以下方式:
父类名.方法(self)
-
这种方式,目前在 Python 3.x 还支持这种方式
-
这种方法 不推荐使用,因为一旦 父类发生变化,方法调用位置的 类名 同样需要修改
-
提示
- 在开发时,父类名 和 super() 两种方式不要混用
- 如果使用 当前子类名 调用方法,会形成递归调用,出现死循环
1.3 父类的 私有属性 和 私有方法
-
子类对象 不能 在自己的方法内部,直接 访问 父类的 私有属性 或 私有方法
-
子类对象 可以通过 父类 的 公有方法 间接 访问到 私有属性 或 私有方法
-
私有属性、方法 是对象的隐私,不对外公开,外界 以及 子类 都不能直接访问
-
私有属性、方法 通常用于做一些内部的事情
-
B 的对象不能直接访问 __num2 属性
-
B 的对象不能在 demo 方法内访问 __num2 属性
-
B 的对象可以在 demo 方法内,调用父类的 test 方法
-
父类的 test 方法内部,能够访问 __num2 属性和 __test 方法
2. 多继承
- 概念
- 子类 可以拥有 多个父类,并且具有 所有父类 的 属性 和 方法
- 例如:孩子 会继承自己 父亲 和 母亲 的 特性
class 子类名(父类名1, 父类名2...)
pass
2.1 多继承的使用注意事项
- 问题的提出
- 如果 不同的父类 中存在 同名的方法,子类对象 在调用方法时,会调用 哪一个父类中的方法呢?
- 提示:开发时,应该尽量避免这种容易产生混淆的情况! —— 如果 父类之间 存在 同名的属性或者方法,应该 尽量避免 使用多继承
- Python 中的 MRO —— 方法搜索顺序(知道)
- Python 中针对 类 提供了一个 内置属性 __mro__ 可以查看 方法 搜索顺序
- MRO 是 method resolution order,主要用于 在多继承时判断 方法、属性 的调用 路径
print(C.__mro__)
# (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
- 在搜索方法时,是按照 mro 的输出结果 从左至右 的顺序查找的
- 如果在当前类中 找到方法,就直接执行,不再搜索
- 如果 没有找到,就查找下一个类 中是否有对应的方法,如果找到,就直接执行,不再搜索
- 如果找到最后一个类,还没有找到方法,程序报错
2.2 新式类与旧式(经典)类
-
object 是 Python 为所有对象提供的 基类,提供有一些内置的属性和方法,可以使用 dir 函数查看
- 新式类:以 object 为基类的类,推荐使用
- 经典类:不以 object 为基类的类,不推荐使用
- 在 Python 3.x 中定义类时,如果没有指定父类,会 默认使用 object 作为该类的 基类 —— Python 3.x 中定义的类都是 新式类
- 在 Python 2.x 中定义类时,如果没有指定父类,则不会以 object 作为 基类
-
新式类 和 经典类 在多继承时 —— 会影响到方法的搜索顺序
-
为了保证编写的代码能够同时在 Python 2.x 和 Python 3.x 运行!
-
今后在定义类时,如果没有父类,建议统一继承自 object
class 类名(object):
pass