本笔记参考视频为:https://www.bilibili/video/BV1ex411x7Em?p=101
,有兴趣的可以直接移步B站
Python编程-高级
- 1 面向对象(OPP)基本概念
- 1.1 过程和函数
- 1.2 面向过程和面向对象基本概念
- 2 类和对象
- 2.1 类和对象的概念
- 2.1.1 类
- 2.1.2 对象
- 2.2 类和对象的关系
- 2.3 类的设计
- 2.3.1 类名的确定
- 2.3.2 属性和方法的确定
- 3 面向对象基础语法
- 3.1 dir内置函数
- 3.2 定义简单的类(只包含方法)
- 3.2.1 定义只包含方法的类
- 3.2.2 创建对象
- 3.2.3 第一个面向对象程序
- 3.3 方法中的self参数
- 3.3.1 案例改造——类的外部给对象增加属性
- 3.3.2 使用self在方法内部输出每一只猫的名字
- 3.4 初始化方法
- 3.4.1 之前代码存在的问题——在类的外部给对象增加属性
- 3.4.2 初始化方法
- 3.4.3 在初始化方法内部定义属性
- 3.4.4 改造初始化方法——初始化的同时设置初始值
- 3.5 内置方法和属性
- 3.5.1 \_\_del\_\_方法(类似析构函数)
- 3.5.2 \_\_str\_\_方法
- 4 面向对象封装案例
- 4.1 封装
- 4.2 小明爱跑步
- 4.2.1 小明爱跑步扩展——小美也爱跑步
- 4.3 摆放家具
- 4.4 封装案例2 —— 士兵突击
- 4.5 身份运算符
- 5 私有属性和私有方法
- 5.1 应用场景及定义方式
- 5.2 伪私有属性和私有方法
- 6 继承
- 6.1 单继承
- 6.1.1 继承的概念、语法和特点
- 6.1.2 方法的重写
- 6.1 3 父类的私有属性和私有方法
- 6.2 多继承
- 6.2.1 多继承的使用注意事项
- 6.2.2 新式类与旧式(经典)类
- 7 多态
- 7.1 多态案例演练
- 8 单例
- 9 异常
- 10 模块和包
- 10.1 模块
- 10.1.1 模块的概念
- 10.1.2 模块的几种导入方式
- 10.1.3 模块的搜索顺序
- 10.1.4 原则——每一个文件都应该是可以被导入的
- 10.2 包
1 面向对象(OPP)基本概念
面向对象编程——Object Oriented Orogramming
简写OOP
- 之前学习的编程方式是面向过程的
- 面向过程和面向对象,是两种不同的编程方式
1.1 过程和函数
- 过程是早期的一个编程概念
- 过程类似于函数,只能执行,但是没有返回值
- 函数不仅能执行,还可以返回结果
1.2 面向过程和面向对象基本概念
- 面向过程
- 把完成某一个需求的
所有步骤
,从头到尾逐步实现 - 根据开发需求,将某些功能独立的代码封装成一个又一个函数
- 最后完成的代码,就是顺序地调用不同的函数
- 特点:
- 注重步骤与过程,不注重职责分工
- 如果需求复杂,代码会变得很复杂
- 开发复杂项目,没有固定的套路,开发难度很大!
- 把完成某一个需求的
- 面向对象
- 相比较函数,面向对象是更大的封装,根据职责在一个对象中封装多个方法
- 在完成某一个需求前,首先确定职责——要做的事情(方法)
- 根据职责确定不同的对象,在对象内部封装不同的方法(多个)
- 最后完成的代码,就是顺序地让不同的对象调用不同的方法
- 特点:
- 注重对象和职责,不同的对象承担不同的职责
- 更加适合应对复杂的需求变化,是专门应对复杂项目开发,提供的固定套路
- 需要在面向过程的基础上,再学习一些面向对象的语法
2 类和对象
2.1 类和对象的概念
类和对象是面向对象编程的两个核心概念
2.1.1 类
- 类是对一群具有相同特征或者行为的事物的一统称,是抽象的,不能直接使用
- 特征被称为属性
- 行为被称为方法
- 类就相当于制造飞机时的图纸,是一个模板,是负责创建对象的
2.1.2 对象
- 对象是由类创建出来的一个具体存在,可以直接使用
- 由哪一个类创建出来的对象,就拥有在哪一个类中定义的属性和方法
- 对象就相当于用图纸制造的飞机
在程序开发中,应该现有类,再有对象
2.2 类和对象的关系
- 类是模板,对象是根据类这个模板创建出来的,应该先有类,再有对象
- 类只有一个,而对象可以有很多个
- 不同的对象之间属性可能会各不相同
- 类中定义了什么属性和方法,对象中就有什么属性和方法,不可能多,也不可能少
2.3 类的设计
在使用面向对象开发前,应该首先分析需求,确定以下程序中需要包含那些类
以植物大战僵尸游戏为例分析类的设计如下图
在程序开发中,要设计一个类,通常需要满足以下三个要素
- 类名:这类事物的名字,满足大驼峰命名法
- 属性:这类事物具有什么样的特征
- 方法:这类事物具有什么样的行为
2.3.1 类名的确定
名词提炼法:分析整个业务流程,出现的名词,通常就是找到的类
2.3.2 属性和方法的确定
- 对对象的特征描述,通常可以定义成属性
- 对象具有的行为(动词),通常可以定义成方法
3 面向对象基础语法
3.1 dir内置函数
在ipython
中快速确认某个东西是对象的方法:
1.在标识符/数据后输一个.
,然后按下TAB
键,ipython会提示该对象能够调用的方法列表(只能看到一些常用的方法列表)
2.使用内置函数dir
传入标识符/数据,可以查看对象内的所有属性及方法
提示:__方法名__
格式的方法是Python提供的内置方法/属性,以下是一些常用的内置方法/属性
序号 | 方法名 | 类型 | 作用 |
---|---|---|---|
01 | __new__ | 方法 | 创建对象时,会被自动调用 |
02 | __init__ | 方法 | 对象被初始化时,会被自动调用 |
03 | __del__ | 方法 | 对象从内存被销毁前,会被自动调用 |
04 | __str__ | 方法 | 返回对象的描述信息,print函数输出使用 |
3.2 定义简单的类(只包含方法)
3.2.1 定义只包含方法的类
语法格式:
class 类名:
def 方法1(self,参数列表):
pass
def 方法2(self,参数列表):
pass
- 方法的定义格式和之前学习过的函数几乎一样
- 区别在于第一个参数必须是
self
3.2.2 创建对象
语法格式:
对象变量 = 类名()
3.2.3 第一个面向对象程序
需求:
小猫爱吃鱼,小猫要喝水
分析:
- 定义一个猫类
Cat
- 定义两个方法
eat
和drink
- 按照需求——不需要定义属性
示例:
# 创建猫类
class Cat:
def eat(self):
print("小猫爱吃鱼")
def drink(self):
print("小猫要喝水")
# 创建一个对象
Tom = Cat()
Tom.drink()
Tom.eat()
结果:
引用概念的强调
在面向对象开发中,引用的概念是同样适用的!
- 在python中使用类创建对象之后,
tom
变量中仍然记录的是对象在内存中的地址 - 也就是
tom
变量引用了新建的猫对象 - 使用
print
输出对象变量,默认情况下,是能够输出这个变量引用的对象是由哪一个类创建的对象,以及在内存中的地址
提示:在计算机中,通常使用十六进制表示内存地址
%d
可以以10进制输出数字%x
可以以16进制输出数字
3.3 方法中的self参数
3.3.1 案例改造——类的外部给对象增加属性
- 在Python中,要给对象设置属性,非常的容易,但是不推荐使用,因为对象属性的封装应该封装在类的内部
- 只需要在类的外部代码中直接通过
.
设置一个属性即可
以上一节中的代码为例,在创建了小猫对象Tom = Cat()
之后,想要从类外部给Tom增加一个姓名属性,可以直接在执行代码出输入Tom.name = "Tom"
即可
3.3.2 使用self在方法内部输出每一只猫的名字
想要在类内部调用某个属性,可以直接self.属性名
哪一个对象调用的方法,self就是哪一个对象的引用。self类似C++里面的this指针
- 在类封装的方法内部,
self
就表示当前调用方法的对象自己 - 调用方法时,程序员不需要传递
self
函数 - 在方法内部
- 可以通过
self.
访问对象的属性 - 也可以通过
self.
调用其他的对象方法
- 可以通过
示例:
# 创建猫类
class Cat:
def eat(self):
print("%s 爱吃鱼" % self.name)
def drink(self):
print("%s 要喝水" % self.name)
# 创建一个对象
Tom = Cat()
Tom.name = "Tom"
Tom.drink()
Tom.eat()
结果:
3.4 初始化方法
3.4.1 之前代码存在的问题——在类的外部给对象增加属性
执行上述程序解释器会报错,因为在执行eat()
和drink()
方法的时候还没有name
属性
3.4.2 初始化方法
- 当使用
类名()
创建对象时,会自动执行以下操作:- 为对象在内存中分配空间——创建对象
- 为对象的属性设置初始值——初始化方法(
init
)
- 这个初始化方法就是
__init__
方法,__init__
是对象的内置方法
__init__
方法是专门用来定义一个类具有那些属性的方法
3.4.3 在初始化方法内部定义属性
- 在
__init__
方法内部使用self.属性名=属性的初始值
就可以定义属性 - 定义属性之后,再使用
Cat
类创建的对象,都会拥有该属性
示例:
# 创建猫类
class Cat:
def __init__(self):
self.name = "Jerry"
def eat(self):
print("%s 爱吃鱼" % self.name)
def drink(self):
print("%s 要喝水" % self.name)
# 创建一个对象
Tom = Cat()
Tom.drink()
Tom.eat()
结果:
3.4.4 改造初始化方法——初始化的同时设置初始值
- 在开发中,如果希望在创建对象的同时,就设置对象的属性,可以对
__init__
方法进行改造- 把希望设置的属性值,定义成
__init__
方法的参数 - 在方法内部使用
self.属性=形参
接受外部传递的参数 - 在创建对象时,使用
类名(属性1, 属性2...)
调用
- 把希望设置的属性值,定义成
示例:
# 创建猫类
class Cat:
def __init__(self, name_):
self.name = name_
def eat(self):
print("%s 爱吃鱼" % self.name)
def drink(self):
print("%s 要喝水" % self.name)
# 创建一个对象
Tom = Cat("汤姆")
Tom.drink()
Tom.eat()
lazy_cat = Cat("大懒猫")
lazy_cat.drink()
lazy_cat.eat()
结果:
3.5 内置方法和属性
序号 | 方法名 | 类型 | 作用 |
---|---|---|---|
01 | __del__ | 方法 | 对象被从内存中销毁前,会被自动调用 |
02 | __str__ | 返回对象的描述信息,print函数输出使用 |
3.5.1 __del__方法(类似析构函数)
- 在Python中
- 当使用
类名()
创建对象时,为对象分配空间后,自动调用__init__
方法 - 当一个对象被从内存中销毁前,会自动调用
__del__
方法
- 当使用
- 应用场景
__init__
改造初始化方法,可以让创建对象更加灵活__del__
如果希望在对象销毁前,再做一些事情,可以考虑以下该方法
- 生命周期
- 一个对象从调用
类名()
创建,生命周期开始 - 一个对象的
__del__
方法一旦被调用,生命周期结束 - 再对象的生命周期内,可以访问对象属性,或者让对象调用方法
- 一个对象从调用
示例:
# 创建猫类
class Cat:
def __init__(self, name):
self.name_ = name
print("%s 来了" % self.name_)
# 定义析构函数
def __del__(self):
print("%s 走了" % self.name_)
# 创建一个对象
tom = Cat("Tom")
print(tom.name_)
print("-" * 20)
结果:
分析:
tom是一个全局变量,只有在整个程序结束之后才会被销毁,因此会先打印----------,在执行析构函数中的语句
Python中有一个关键字
del
,可以直接销毁变量,若在打印-------之前销毁tom变量,则会先执行析构函数中的语句,代码如下:
# 创建猫类
class Cat:
def __init__(self, name):
self.name_ = name
print("%s 来了" % self.name_)
# 定义析构函数
def __del__(self):
print("%s 走了" % self.name_)
# 创建一个对象
tom = Cat("Tom")
print(tom.name_)
del(tom)
print("-" * 20)
结果:
3.5.2 __str__方法
- 在Python中,使用
print
输出对象变量,默认情况下,会输出这个变量引用的对象是由哪一个类创建的对象,以及在内存中的地址(十六进制表示)[见3.2.3] - 如果在开发中,希望使用
print
输出对象变量时能够打印自定义的内容,就可以利用__str__
这个内置方法了
注意:
__str__
方法必须返回一个字符串
# 创建猫类
class Cat:
def __init__(self, name):
self.name_ = name
print("%s 来了" % self.name_)
# 定义析构函数
def __del__(self):
print("%s 走了" % self.name_)
def __str__(self):
# 必须返回一个字符串
return "我是小猫 %s" % self.name_
# 创建一个对象
tom = Cat("Tom")
print(tom)
如上例,在使用
tom
时,输出了__str__
中返回的内容
4 面向对象封装案例
4.1 封装
- 封装时面向对象编程的一大特点
- 面向对象编程的第一步 —— 将属性和方法封装到一个抽象的类中
- 外界使用类创建对象,然后让对象调用方法
- 对象方法的细节都被封装在类的内部
4.2 小明爱跑步
需求
- 小明 体重
75.0
公斤 - 小明每次跑步会减肥0.5公斤
- 小明每次吃东西体重会增加1公斤
示例:
class Person:
def __init__(self, name, weight) -> None:
self.name_ = name
self.weight_ = weight
def __str__(self) -> str:
return "我的名字是 %s,我的体重是 %.2f 公斤" % (self.name_, self.weight_)
def run(self):
print("跑步好,跑步减肥")
self.weight_ -= 0.5
def eat(self):
print("没吃饱哪有力气减肥")
self.weight_ += 1
xiaoming = Person("小明", 75.0)
xiaoming.eat()
xiaoming.run()
print(xiaoming)
结果:
4.2.1 小明爱跑步扩展——小美也爱跑步
需求:
- 小明和小美都爱跑步
- 小明体重75.0公斤
- 小美体重45.0公斤
- 每次跑步都会减少0.5公斤
- 每次吃东西都会增加1公斤
示例:
class Person:
def __init__(self, name, weight) -> None:
self.name_ = name
self.weight_ = weight
def __str__(self) -> str:
return "我的名字是 %s,我的体重是 %.2f 公斤" % (self.name_, self.weight_)
def run(self):
print("跑步好,跑步减肥")
self.weight_ -= 0.5
def eat(self):
print("没吃饱哪有力气减肥")
self.weight_ += 1
xiaoming = Person("小明", 75.0)
xiaoming.eat()
xiaoming.run()
print(xiaoming)
xiaomei = Person("小美", 45.0)
xiaomei.eat()
xiaomei.run()
print(xiaomei)
结果:
4.3 摆放家具
需求
- 房子(House)有户型、总面积和家具名称列表
- 新房子中没有任何的家具
- 家具(HouseItem)有名字和占地面积,其中
- 席梦思(bed)占地4平米
- 衣柜(chest)占地2平米
- 餐桌(table)占地1.5平米
- 将以上三件家具添加到房子中
- 打印房子时,要求输出户型、总面积、剩余面积、家具名称列表
示例:
class HouseItem:
def __init__(self, name, area) -> None:
self.name = name
self.area = area
def __str__(self) -> str:
return "[%s] 占地 %.2f " % (self.name, self.area)
class House:
def __init__(self,house_type, area) -> None:
self.house_type = house_type
self.area = area
# 剩余面积
self.free_area = area
# 家具列表
self.item_list = []
def __str__(self) -> str:
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)
# 判断能否装下
if item.area > self.free_area:
print("家具太大,房子装不下")
return
# 更新家具列表
self.item_list.append(item.name)
# 更新剩余面积
self.free_area -= item.area
bed = HouseItem("席梦思", 4)
chest = HouseItem("衣柜", 2)
table = HouseItem("餐桌",1.5)
my_home = House("两室一厅",60)
my_home.add_item(bed)
my_home.add_item(chest)
my_home.add_item(table)
print(my_home)
结果:
4.4 封装案例2 —— 士兵突击
需求:
- 士兵 许三多有一把AK47
- 士兵可以开火
- 枪能够发射子弹
- 枪装填子弹——增加子弹数量
定义没有初始值的属性
在定义属性时,如果不知道设置什么初始值,可以设置为None
None
关键字表示什么都没有- 表示一个空对象,没有方法和属性,是一个特殊的变量
- 可以将
None
赋值给任何一个变量
fire
方法需求
- 判断是否有枪,没有枪没法冲锋
- 喊一声口号
- 装填子弹
- 射击
示例:
class Gun:
def __init__(self, type) -> None:
self.type = type
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] biubiubiu~! [%d]" % (self.type, self.bullet_count))
class Soldier:
def __init__(self, name) -> None:
self.name = name
# 新兵一开始没有枪
self.gun = None
def fire(self):
# 判断是否有枪
if self.gun == None:
print("[%s] 还没有枪" % self.name)
return
# 喊一声口号
print("同志们,冲啊!!")
# 装填子弹
self.gun.add_bullet(40)
# 射击
self.gun.shoot()
ak47 = Gun("AK47")
xusanduo = Soldier("许三多")
xusanduo.gun = ak47
xusanduo.fire()
结果:
4.5 身份运算符
身份运算符用于比较两个对象的内存地址是否一致——是否是同一个对象的引用
- 在
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
用于判断两个变量引用对象是否为同一个
==
用于判断引用变量的值是否相等
5 私有属性和私有方法
5.1 应用场景及定义方式
应用场景:
- 在实际开发中,对象的某些属性或方法可能只希望在对象的内部被使用,而不希望再外部被访问到
- 私有属性就是对象不希望公开的属性
- 私有方法就是对象不希望公开的方法
定义方式:
在定义属性或方法时,在属性名或者方法名前增加两个下划线,定义的就是私有属性或方法
5.2 伪私有属性和私有方法
提示:在日常开发中,不要使用这种方式,访问对象的私有属性或私有方法
在Python中,并没有真正意义的私有
- 在给属性、方命名时,实际是对名称做了一些特殊处理,使得外界无法访问
- 解释器的处理方式:在名称前面加上
_类名
=>_类名_名称
6 继承
面向对象三大特性:
- 封装根据职责将属性和方法封装到一个抽象类中
- 继承实现代码的重用,相同的代码不需要重复的编写
- 多态不同的对象调用相同的方法,产生不同的执行结果,增加代码的灵活度
6.1 单继承
6.1.1 继承的概念、语法和特点
继承的概念:子类拥有父类的所有方法和属性
1)继承的语法
class 类名(父类名):
- 子类继承自父类,可以直接享受父类中已经封装好的方法,不需要再次开发
- 子类中应该根据职责,封装子类特有的属性和方法
2)专业术语
Dog
类是Animal
类的子类,Animal
类是Dog
类的父类,Dog
类从Animal
类继承Dog
类是Animal
类的派生类,Animal
类是Dog
类的基类,Dog
类从Animal
类派生
3)继承的传递性
- C类从B类继承,B类又从A类继承
- 那么C类就有B类和A类的所有属性和方法
子类拥有父类以及父类的父类中封装的所有属性和方法
6.1.2 方法的重写
应用场景
当父类的方法实现不能满足子类需求时,可以对方法进行重写(overwrite)
重写父类方法有两种情况
- 覆盖父类的方法
- 对父类的方法进行扩展
1)覆盖父类的方法
- 如果在开发中,父类的方法实现和子类的方法实现,完全不同
- 就可以使用覆盖的方式,在子类中重新编写父类的方法实现
具体的实现方式,就相当于在子类中定义了一个和父类同名的方法并且实现
重写之后,在运行时,只会调用子类中重写的方法,而不再会调用父类封装的方法
2)对父类方法进行扩展
- 如果在开发中,子类的方法实现中包含父类的方法实现
- 父类原本封装的方法实现是子类方法的一部分
- 就可以使用扩展的方式
- 在子类中重写父类的方法
- 在需要的位置使用
super().父类方法
来调用父类方法的执行 - 代码其他的位置针对子类的需求,编写子类特有的代码实现
关于super
- 在Python中,
super
是一个特殊的类 super()
就是使用super
类创建出来的对象- 最常使用的场景就是在重写父类方法时,调用在父类中封装的方法实现
示例:
class Animal:
def eat(self):
print("吃!")
def drink(self):
print("喝!")
def run(self):
print("跑!")
def sleep(self):
print("睡!")
class Dog(Animal):
def bark(self):
print("汪汪汪")
class XiaoTianQuan(Dog):
def bark(self):
# 针对子类特有的需求,编写代码
print("哮天犬叫!")
# 使用super(). 调用原本在父类中封装的方法
super().bark()
# 增加其他子类的代码
print("吃月亮!")
xtq = XiaoTianQuan()
xtq.bark()
结果:
调用父类方法的另外一种方式:
在Python2.x中,如果需要调用父类的方法,还可以使用以下方式:
父类名.方法(self)
- 这种方式,目前在
Pyrhon3.x
还支持这种方式 - 这种方式不推荐使用,因为一旦父类发生变化,方法调用位置的类名同样需要修改
提示
- 在开发时,
父类名
和super()
两种方式不要混用 - 如果使用当前子类名调用方法,会形成递归调用,出现死循环
6.1 3 父类的私有属性和私有方法
- 子类对象不能在自己方法内部,直接访问父类的私有属性或私有方法
- 子类对象可以通过父类的公有方法间接访问到私有属性或私有方法
- 私有属性、方法时对象的隐私,不对外公开,外界以及子类都不能直接访问
- 私有属性、方法通常用于做一些内部的事情
B
的对象不能直接访问__num2
属性B
的对象不能再demo
方法内访问__num2
属性B
的对象不可以在demo
方法内,调用父类的test
方法- 父类的
test
方法内部,能够访问__num2
属性和__test
方法
6.2 多继承
概念
- 子类可以拥有多个父亲,并且具有所有父类的属性和方法
语法
class 子类名(父类名1, 父类名2...):
6.2.1 多继承的使用注意事项
问题提出:
如果不同的父类中存在同名的方法,子类对象在调用方法时,会调用哪一个父类中的方法呢?
提示:在开发时,应该尽量避免这种容易产生混淆的情况!如果父类之间存在同名的属性或者方法,应该尽量避免使用多继承
Python中的MRO——方法搜索顺序
- Python中针对类提供了一个内置属性
__mro__
可以查看方法的搜索顺序 - MRO指
method resolution order
, 主要用于在多继承判断方法、属性的调用路径
print(C.__mro__)
- 在搜索方法时,是按照
__mro__
的输出结果从左至右的顺序查找的 - 如果在当前类中找到方法,就直接执行,不在搜索
- 如果没有找到,就查找下一个类中是否有对应的方法,如果找到,就直接执行,不再搜索
- 如果找到最后一个类,还是没有找到方法没程序报错
示例:
class A:
def test(self):
print("A -- test")
def demo(self):
print("A -- demo")
class B:
def test(self):
print("B -- test")
def demo(self):
print("B -- demo")
class C(B, A):
pass
c = C()
c.test()
c.demo()
print(C.__mro__)
结果:
分析:
首先看print(C.__mro__)
的结果,其顺序是自己的类,继承的父类B,继承的父类A,object类(所有类的基类),所以在调用test()
和demo()
时,现在自己类里面找,没有,再去父类B找,找到了,执行
6.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
7 多态
面向对象三大特性
- 封装根据职责将属性和方法封装到一个抽象的类中
- 继承实现代码的重用,相同的代码不需要重复的编写
- 多态不同的子类对象调用相同的父类方法,产生不同的执行结果
- 多态可以增加代码的灵活度
- 以继承和重写父类方法为前提
- 是调用方法的技巧,不会影响到类的内部设计
7.1 多态案例演练
需求
- 在
Dog
类中封装方法game
- 普通狗只是简单的玩耍
- 定义
XiaoTianDog
继承自Dog
,并且重写game
方法- 哮天犬需要在天上玩耍
- 定义
Person
类,并且封装一个和狗玩的方法- 在方法内部,直接让狗对象调用
game
方法
- 在方法内部,直接让狗对象调用
示例:
from pyexpat.model import XML_CTYPE_SEQ
from unicodedata import name
class Dog:
def __init__(self, name) -> None:
self.name = name
def game(self):
print("%s 蹦蹦跳跳地玩耍.." % self.name)
class XiaoTianDog(Dog):
def game(self):
print("%s 飞到天上去玩耍.." % self.name)
class Person:
def __init__(self, name) -> None:
self.name = name
def game_with_dog(self, dog):
print("%s 和 %s 快乐地玩耍.." % (self.name, dog.name))
dog.game()
wangcai = Dog("旺财")
xtq = XiaoTianDog("哮天犬")
xiaoming = Person("小明")
xiaoming.game_with_dog(wangcai)
xiaoming.game_with_dog(xtq)
结果:
分析:
在Person
的方法里,写的其实是和父类的Dog
玩耍,而解释器会根据实际传入的对象来决定调用父类还是某一个子类,这就是多态
8 单例
9 异常
10 模块和包
10.1 模块
10.1.1 模块的概念
- 模块是Python程序架构的一个核心概念
- 每一个以扩展名
py
结尾的Python
源码文件都是以恶搞模块 - 模块名同样也是一个标识符,需要负和标识符的命名规则
- 在模块中定义的全局变量、函数、类都是提供给外界直接使用的工具
- 模块就好比是工具包,要想使用这个工具包中的工具,就需要先导入这个模块
- 每一个以扩展名
10.1.2 模块的几种导入方式
-
import
导入import 模块名1, 模块名2
(不推荐)- 根据Python最新的书写规范,每个导入的模块应该单独占一行
import 模块1
import 模块2
- 导入之后
- 使用
模块名.
的方式就可以使用模块提供的工具——全局变量、函数、类
- 使用
- import导入时指定别名
- 如果模块的名字太长,可以使用
as
指定模块的名称,以方便在代码中使用 import 模块名1 as 模块别名
-
注意:模块别名应该符合大驼峰命名法
- 如果模块的名字太长,可以使用
-
from..import
导入- 如果希望从某一个模块中,导入部分工具,就可以使用
from...import
的方式 import 模块名
是一次性把模块中所有的工具全部导入,并且通过模块名/别名 访问from 模块名1 import 工具名
- 导入之后
- 不需要通过
模块名.
- 可以直接使用模块提供的工具——全局变量、函数、类
- 不需要通过
-
注意:如果两个模块存在同名的函数,那么后导入模块的函数,会覆盖掉先导入的函数
- 开发时
import
代码应该统一写在代码的顶部,更容易及时发现冲突 - 一旦发现冲突,可以使用
as
关键字给其中一个工具起一个别名
- 如果希望从某一个模块中,导入部分工具,就可以使用
-
from...import *
一次性导入所有工具from 模块名1 import *
注意:这种方式不推荐使用,因为函数重名并没有任何的提示,出现问题不好排查、
- 方法3与方法1的区别:(同样是一次性导入所有工具)
- 方法1在调用时需要使用
模块名.
- 方法3可以直接使用所导入的全局变量、函数、类
- 方法1在调用时需要使用
10.1.3 模块的搜索顺序
Python解释器在导入模块时,会:
- 搜索当前目录指定模块名的文件,如果有就直接导入
- 如果没有,再搜索系统目录
注意:在开发时,给文件起名,不要和系统的模块文件重名
Python中每一个模块都有一个内置属性_file_
可以查看模块的完整路径
10.1.4 原则——每一个文件都应该是可以被导入的
- 每一个独立的python文件就是一个模块
- 在导入文件时,文件中所有没有任何缩进的代码都会被执行一遍
实际开发场景
- 在实际开发中,每一个模块都是独立开发的,大多都有专人负责
- 开发人员通常会在模块下方增加一些测试代码
- 仅在模块内部使用,而被导入到其他文件中不需要执行
通过上述描述,发现一个矛盾点:正常自己写代码时候,会有执行代码,但是当这一部分代码被当作模块使用时,只需要全局变量、函数和类,执行代码不需要。单上在
import
这个模块时,会把不需要的执行代码全部运行一遍
目标:找到一种方式,可以在单独测试代码的时候正常运行,而当作模块使用的时候不执行测试代码
-
__name__
属性__name__
属性可以做到,测试模块的代码只在测试情况下被运行,而在被导入时不会被执行__name__
是Python的一个内置属性,记录着一个字符串- 如果是被其他备件导入的,
__name__
就是模块名 - 如果是当前执行的程序,
__name__
就是__main__
-
通过这个属性,可以判断文件的代码是用于测试还是被当作模块,因此可以把测试代码放在如下的一个if语句下
def main(): # 测试代码 pass if __name__ = '__main__': main()
10.2 包
- 概念
- 包是一个包含多个模块的特殊目录
- 目录下有一个特殊的文件
__init__. py
- 包名的命名方式和变量名一致,小写字母+
_
- 好处
- 使用
import 包名
可以一次性导入包中所有的模块
- 使用
__init__.py
- 要在外界中使用包中的模块,需要在
__init__.py
中指定对外界提供的模块列表# 从 当前目录 导入 模块列表 from . import 模块名1 from . import 模块名2
- 要在外界中使用包中的模块,需要在
更多推荐
Python自学笔记——高级篇(面向对象)
发布评论