5. 面向对象三大特性
继承、多肽、封装
5.1 继承
类的继承与现实生活中的父、子继承关系一样。被继承的类称为父类、基类或超类,继承者称为子类。Python 继承分为:单继承和多继承(继承多个类)。
class Parent1:
pass
class Parent2:
pass
class Sub1(Parent1):
pass
class Sub2(parent1, Parent2):
pass
class Dad:
"""父类"""
money = 100000
def __init__(self, name):
print('爸爸')
self.name = name
def hit_son(self):
print('%s正在打儿子' % self.name)
class Son(Dad):
pass
print(Son.money) 100000
Son.hit_son() # hit_son() missing 1 required positional argument: 'self'
# hit_son() 有一个位置参数 self,使用类名调用,不能传入 self,只能用实例调用
print(Dad.__dict__)
print(Son.__dict__)
s1 = Son('tom') # 爸爸
print(s1.money) # 100000
s1.hit_son() # tom正在打儿子
print(s1.__dict__) # {'name': 'tom'}
子类继承父类的所有属性,调用父类的方法只能用实例调用。
class Son(Dad):
money = 9000
def __init__(self, name, age):
self.name = name
self.age = age
def hit_son(self):
print('来自子类')
s2 = Son('rose', 18)
print(s2.money)
print(Dad.money)
s2.hit_son()
9000
100000
来自子类
如果子类中定义与父类同名的方法或属性,不会覆盖父类对应的方法或属性,只是优先调用自己类本身的方法或属性。
5.1.1 什么时候用继承
- 当类之间有显著的不同,并且较小的类是较大类所需组件时,用组合比较好
- 如:一个机器人可以是一个类,它的手臂、腿和身体等也都可以是一个类,它们互不相关
- 当类之间有很多相同功能,提取这些共同的功能做成基类,用继承比较好
- 如:猫、狗都要吃喝拉撒,同时也有自己独特的声音,提取它们的共性作为基类
class Cat:
def 喵喵叫(self):
pass
def 吃(self):
pass
def 喝(self):
pass
class Dog:
def 汪汪叫(self):
pass
def 吃(self):
pass
def 喝(self):
pass
狗和猫都属于动物,提取它们的共性,做成基类:
class Animal:
def 吃(self):
pass
def 喝(self):
pass
class Cat(Animal):
def 喵喵叫(self): # 派生
pass
class Dog(Animal):
def 汪汪叫(self): # 派生
pass
喵喵叫、汪汪叫是派生而来的,吃喝拉撒是继承而来
派生(衍生):子类在继承父类的基础上衍生出新的属性,如:Cat 的喵喵叫、Dog 的汪汪叫。也可以是子类定义与父类重名的属性,子类也称为派生类。
5.1.2 继承的两种含义
- 继承基类的方法,并且做出自己的改变(派生)或拓展(代码重用)
- 这种方法应尽量少用,因为子类于基类出现强耦合,容易出现未知错误。
- 声明某个子类兼容于某基类,定义一个接口类,子类继承接口类,并且实现接口中定义的方法。—— 接口继承
5.1.3 接口继承与归一化
接口继承:
抽象:即提取类似部分
接口:基类的函数属性,只提供函数名,不实现具体功能
定义一个基类,在基类中利用装饰器 (@abc.abstractclassmethod
)将自己的函数属性定义成接口函数,在子类中必须实现该接口函数,否则无法实例化。
接口继承实质上是要求 做出一个良好的抽象,这个抽象规定了一个兼容接口,使得外部调用者无需关心具体细节,可一视同仁的处理实现了特定接口的所有对象 —— 这在程序上,叫做归一化。
归一化使的高层的外部使用者可以不加区分的处理所有接口兼容的对象集合 —— 就好比 linux 的泛文件概念一样,所有东西都可以当文件处理,不必关心它是内存、硬盘、网络还是屏幕(对于底层设计者来说,也可以区分出 字符串设备 和 块设备,然后做出针对性的设计,细致到什么程度,视需求而定)。
import abc
class All_file(metaclass=abc.ABCMeta): # 基类
@abc.abstractmethod # 装饰器定义两个接口函数 read、write
def read(self): # 那么子类必须实现这两个函数,否无法实例化
pass
@abc.abstractmethod
def write(self):
pass
class Disk(All_file):
def read(self):
print('disk read')
def write(self):
print('disk write')
class Memory(All_file):
def read(self):
print('memory read')
d1 = Disk()
d1.read()
d1.write()
m1 = Memory()
m1.read()
disk read
disk write
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-15-53d88a516ad1> in <module>()
23 d1.write()
24
---> 25 m1 = Memory()
26 m1.read()
TypeError: Can't instantiate abstract class Memory with abstract methods write
可以看出 Disk 子类实现了基类的接口函数,可以正常实例化并调用函数属性。而 Memory 子类,没有实现 Write()
,实例化失败。
5.1.4 继承顺序
Python 可以继承多个类,c#、java 只能继承一个。如果继承多个,那么其寻找方式有两种:深度优先和广度优先。
- 当类是经典类时,多继承按照深度优先查找
- 当类是新式类时(Python 3.x 都是新式类),多继承按照广度优先查找
问题:Python 是如何实现继承的?按照什么样的方式去查找的呢?
对于定义的每一个类,Python 会计算出一个方法解析顺序(MRO)元组,它是一个简单的所有基类的线性顺序元组,即一旦实现了继承。就已经计算好查找顺序,并存储在元组中。查看方式:
Class.__mro__ # F.__mro__
为了实现继承,Python 会在 MRO 元组上从左到右开始查找基类,直至找到第一个匹配这个属性的类位置。
而这个 MRO 元组的构造是通过一个 C3 线性化算法来实现的。实际上就是合并所有父类的 MRO 元组并遵循如下三条准则:
- 子类会优先父类被检查:即子类与父类有同样的方法时,优先检查子类
- 多个父类会根据它们在列表中的顺序被检查:
F(D, E)
D 要优先 E - 如果对下一个类存在两个合法的选择,选择第一个父类
class A(object):
def test(self):
print('A')
class B(A):
def test(self):
print('B')
class C(A):
def test(self):
print('C')
class D(B):
def test(self):
print('E')
class E(C):
def test(self):
print('E')
class F(D, E):
def test(self):
print('F')
f1 = F()
f1.test()
print(F.__mro__) # F --> D --> B --> E --> C --> A --> object
F
(<class '__main__.F'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
在 Python 2.x 中有新式类和经典类之分,新式类与 Python3.x 的查找顺序一样。而经典类的查找顺序为:F --> D --> B --> A --> E --> C
。
5.1.5 子类中调用父类的方法
如果想在子类中调用父类的方法(函数属性),就需要重写父类方法,否则会报错:
class Vehicle:
"""交通工具"""
Country = 'China'
def __init__(self, name, speed, load, power):
"""名字、速度、负载、能量"""
self.name = name
self.speed = speed
self.load = load
self.power = power
def run(self):
print('%s今天开通~' % self.name)
class Subway(Vehicle):
"""地铁"""
def __init__(self, line):
"""名字、速度、负载、能量、几号线"""
self.line = line
def test():
pass
line1 = Subway(1)
line1.run() # AttributeError: 'Subway' object has no attribute 'name'
子类 Subway
中调用父类 Vehicle
的 run 方法,报 AttributeError: 'Subway' object has no attribute 'name'
。显然调用没有成功。
那么我们可以重写父类的方法:
class Subway(Vehicle):
"""地铁"""
def __init__(self, name, speed, load, power, line): # 调用父类的属性
"""名字、速度、负载、能量、几号线"""
self.name = name
self.speed = speed
self.load = load
self.power = power
self.line = line
def run(self):
print('%s今天开通~' % self.name)
print('%s%s号线开通啦~' % (self.name, self.line))
line1 = Subway('长沙地铁', '200km/h', 1000, 'electric', 1)
line1.run() # 调用与父类同名的方法
------------------------------
长沙地铁今天开通~
长沙地铁1号线开通啦~
当子类中与父类方法同名时,优先调用子类本身的。但是如果也想实现父类中的功能,就需要在子类相应的方法中重写,若有几十上百行代码,重写就显得有点麻烦。
Python 给我们提供了两种解决方法:
- 调用未绑定的父类方法
- 使用 super 函数
调用未绑定的父类方法
只需在子类中需要调用父类的方法中,加上这样一行代码即可:
类名.__init__(self, x, y...) # 调用父类中的变量
类名.test(self) # 调用父类方法
具体示例如下:
class Subway(Vehicle):
"""地铁"""
def __init__(self, name, speed, load, power, line):
"""名字、速度、负载、能量、几号线"""
Vehicle.__init__(self, name, speed, load, power) # 调用几个变量就添加几个
self.line = line
def run(self):
Vehicle.run(self)
print('%s%s号线开通啦~' % (self.name, self.line))
line1 = Subway('长沙地铁', '200km/h', 1000, 'electric', 1)
line1.run()
----------------------------------------
长沙地铁今天开通~
长沙地铁1号线开通啦~
5.1.6 super 函数
在子类中调用父类方法,使用上面的方法,有个缺点:一旦父类名字变化,后面调用逻辑就需要大量更改被继承的方法。Python 还提供了另一种方法:super()函数。
它可以自动找到父类的方法,并且自动传入 self 参数。无需关心父类的名字,不需要去大量修改被继承的方法:
super().meth([args])
class Subway(Vehicle):
"""地铁"""
def __init__(self, name, speed, load, power, line):
"""名字、速度、负载、能量、几号线"""
super().__init__(name, speed, load, power)
self.line = line
def run(self):
super.run()
print('%s%s号线开通啦~' % (self.name, self.line))
line1 = Subway('长沙地铁', '200km/h', 1000, 'electric', 1)
line1.run()
----------------------------------------
长沙地铁今天开通~
长沙地铁1号线开通啦~
class Foo:
def run(self):
return 'is running....'
class Bar(Foo):
def __init__(self):
super(Bar, self).__init__()
print(self.run())
b = Bar()
print(b.run())
5.2 多肽
由不同的类实例化得到的对象,调用同一个方法,执行的逻辑不同。对象如何通过他们共同的属性和动作来操作及访问,而不考虑他们具体的类。
#_*_coding:utf-8_*_
__author__ = 'Hubery_Jun'
class H2O:
def __init__(self, name, temperature):
self.name = name
self.temperature = temperature
def turn_ice(self):
if self.temperature < 0:
print('[%s]水温太低,结冰了' % self.name)
elif self.temperature > 0 and self.temperature < 100:
print('[%s]液态化成水' % self.name)
elif self.temperature > 100:
print('[%s]温度太高变成水蒸气了' % self.name)
class Water(H2O):
pass
class Ice(H2O):
pass
class Steam(H2O):
pass
w1 = Water('水', 25)
i1 = Water('冰', -25)
s1 = Water('水蒸气', 3000)
# w1.turn_ice()
# i1.turn_ice()
# s1.turn_ice()
def func(obj):
obj.turn_ice()
func(w1)
func(i1)
func(s1)
[水]液态化成水
[冰]水温太低,结冰了
[水蒸气]温度太高变成水蒸气了
对象 w1、i1、s1
由不同的类实例而来,但是它们调用同一个方法 turn_ice()
。我们可以定义一个函数 func
,专门用来处理 turn_ice()
方法,不需要关心类内部逻辑,只需考虑对象的类型,这就是多肽。
5.3 封装
5.3.1 公有私有数据属性
一般面向对象编程语言都有公有私有数据属性,Java、C++ 使用 public 和 private 关键字来区分。而 Python 默认对象的属性和方法都是公开的(即公有的),可以直接通过点(.)来访问。
class Person:
name = 'rose'
def __init__(self, age, gender):
self.age = age
self.gender = gender
p1 = Person(18, 'female')
print(p1.name)
----------------
rose
为了实现类似私有变量的特性,Python 有一种约束,可以对要私有化的变量,在其前面加一条下划线(_)。但是从外部也可以访问,这是一种约束,约束看到这种变量就不去调用:
_name = 'rose'
Python 内部采用一种叫 name mangling(名字改编)
的技术,只需在变量名前加双下划线即可(__),那么这个变量就变成了私有变量:
__name = 'rose'
print(p1.__name) # AttributeError: 'Person' object has no attribute '_name'
其实 Python 目前的私有机制是伪私有,在外部还是可以通过 _类名__变量名
的方式去访问私有变量:
print(p1._Person__rose())
print(Person.__dict__)
{'__module__': '__main__', '_Person__name': 'rose', '__init__': <function Person.__init__ at 0x00000000053D6598>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
rose
可以发现在 Python 内部,将原有的变量名改变了名字:(__rose
变成了 _Person__name
。
5.3.2 封装的三个层面
-
类本身就是对数据和函数的一种封装。
-
另一种封装类中定义私有属性,内部能访问,外部不能访问。
-
明确区分内外,内部的实现逻辑,外部无法知晓。并且为封装到内部的逻辑提供一个访问接口给外部使用(这才是真正的封装)
class Person:
name = 'rose'
def __init__(self, age, gender):
self.__age = age
self.__gender = gender
# 接口函数
def interface_func(self):
return self.__age, self.__gender
p1 = Person(18, 'female')
print(p1.interface_func())
--------------------------
(18, 'female')
对于类中已经封装的方法,只能在内部访问,外部要想访问。我们可以定义一个接口函数,然后调用接口函数间接使用封装的方法。但是在实际开发中应谨慎使用接口函数,滥用接口函数就会导致类 千疮百孔。
5.4 总结
面向对象是一种更高级的结构化编程方式,其优点有两点:
- 通过封装,明确内外。外部调用无需知道内部逻辑。
- 通过继承和多肽在语言层面支持归一化设计
6. 反射
反射是指程序可以访问、检查和修改它本身状态或行为的一种能力(自省)。Python 内部实现自省的四个函数:hasattr()、getattr()、setattr()、delattr()
。
6.1.1 实现反射的四个函数
class Animal:
def __init__(self, name, color):
self.name = name
self.color = color
def eat(self):
print('【%s】正则吃东西' % self.name)
a1 = Animal('dog', 'black')
1. hasattr(object, name)
判断一个对象中是否有指定的属性,第一个参数为对象,第二个为属性名。
print(hasattr(a1, 'name')) # True
print(hasattr(a1, 'eat')) # True
print(hasattr(a1, 'edfa')) # False
2. getattr(object, name[,default])
返回对象的指定属性,若属性不存在且未指定默认值,抛出 AttributeError
异常。指定默认值返回默认值。
print(getattr(a1, 'name')) # dog
print(getattr(a1, 'eat')) # <bound method Animal.eat of <__main__.Animal object at 0x00000000025ACEF0>>
#print(getattr(a1, 'edfa')) # AttributeError: 'Animal' object has no attribute 'edfa'
print(getattr(a1, 'edfa', '没有找到')) # 没有找到
3. setattr(object, name, value)
设置对象中指定属性的值,如属性不存在则新增,存在则修改。
# a1.name = 'cat' # 常规修改方法
setattr(a1, 'name', 'cat') # 修改
setattr(a1, 'age', 1) # 新增
4. delattr(object, name)
删除对象指定的属性,属性不存在抛出 AttributeError
异常。
# a1.name # 常规修改方法
delattr(a1, 'name')
6.1.2 反射的应用场景
反射的有两大好处:一是可以实现可插拔机制,二是动态导入模块(基于反射当前模块成员)
实现可插拔机制
比如说有两个程序员,分别负责开发两个模块ftp_client.py、user.py
。user.py
依赖 ftp_client.py
。若哪天负责开发 ftp_client.py
的程序员突然去有事了,为了不影响整体进度,我们可以利用反射判断 ftp_client.py
是否实现了某个功能,即使没实现也不会报错:
# ftp_client.py
class FtpClient:
'ftp 客户端,具体功能尚未实现'
def __init__(self, addr):
print('正在连接服务器【%s】' % addr)
self.addr = addr
利用 hasattr()
判断 FtpClient 类
中是否有 put 方法,若有则获取,否则继续实现其他功能逻辑:
# user.py
from ftp_client import FtpClient
f1 = FtpClient('8.8.8.8')
if hasattr(f1, 'put'):
func_put = getattr(f1, 'put')
func_put()
else:
print('put 功能尚未实现')
print('其他功能逻辑')
动态导入模块
若导入的模块是字符串形式,我们之前可以采用 __import__()
函数导入。
包 h 下有个 a1 模块,要想在另一个模块中调用 a1 中的 test()
函数:
r = __import__('h.a1')
r.a1.test()
另一种方式是利用 importlib 模块
,动态导入模块:
import importlib
r = importlib.import_module('h.a1')
r.test()
6.1.3 __getattr__、__setarrr__、__delattr__
__getattr__
当获取不存在的属性时触发:
class Foo:
x = 1
def __init__(self, y):
self.y = y
def __getattr__(self, item):
print('属性【%s】不存在~' % item)
f1 = Foo(10)
print(f1.y) # 10
print(f1.s) # 属性【s】不存在~、None
对象 f1 传给 self,y 或 s 传给 item。
__setattr__
设置属性时触发
def __setattr__(self, key, value):
print('正在设置属性~')
# self.key = value
# RecursionError: maximum recursion depth exceeded in comparison(超出最大递归深度)
self.__dict__[key] = value
f1 = Foo(10)
f1.m = 20
print(f1.__dict__)
# 正在设置属性~
# 正在设置属性~
# {'y': 10, 'm': 20}
__delattr__
删除属性时触发
def __delattr__(self, item):
print('正在删除~')
f1 = Foo(10)
del f1.y
del f1.x
# 正在删除~
# 正在删除~
7. 二次加工标准类型(包装)
Python 提供了很多内置的标准数据类型,以及内置方法。在很多场景下我们需要基于标准数据类型来定制我们自己的类型,新增或改写方法。我们可以用继承/派生来对标准类型进行二次加工:
需求:
基于 list 新增一个方法用于返回列表中间元素,修改 list 内置的 append 方法,要求只有当新增的元素是字符串时才能添加进去。
class List(list):
"""继承 list 所有的属性,也派生自己的,比如 append,mid"""
def append(self, str_obj):
"""派生自己的 append:类型检查"""
if isinstance(str_obj, str):
super().append(str_obj) # 调用父类的 append()方法,调用自己的会无限递归直至溢出
else:
print('必须是字符串类型')
def mid(self):
"""新增属性"""
middle = int(len(self)/2)
return '列表%s,中间元素是【%s】' % (self, self[middle])
# 新增属性
# List 继承 list,那么 List('hello')相当于 list('hello') ,self 为实例本身,那么 self = list(‘heelo')’)
l1 = List('hello') # 列表['h', 'e', 'l', 'l', 'o'],中间元素是【l】
l1.mid()
# # 派生自己的 append
# l2 = List('hello')
# l2.append('2')
# print(l2) # ['h', 'e', 'l', 'l', 'o', '2']
# l2.append(3) # 必须是字符串类型
授权
授权是包装的另一特性,它与包装的区别在于:
- 包装: 通常是对已存在的类型进行定制,可新建、修改和删除原有类型的功能,其他则不变。(继承和派生)
- 授权: 对于已存在的功能授权给对象的默认属性,更新的功能交给类的某些方法来处理。 (组合)
授权的关键点在于覆盖 __getattr__
需求: 模拟 open 函数的read、write 方法,要求 read 方法交给类方法处理,write 方法授权给对象默认属性:
read 方法
class FileHandle:
"""模拟 open 函数"""
def __init__(self, file, mode='r', encoding='utf-8'):
self.file = open(file, mode, encoding=encoding) # 文件句柄或文件对象 <_io.TextIOWrapper name='a.txt' mode='r+' encoding='utf-8'>
def __getattr__(self, item):
# print('不存在')
print(self, item) # self:f1,item:'read'
return getattr(self.file, item) # 在文件对象中查找 字符串 read
f1 = FileHandle('a.txt', 'r+')
print(f1.read()) # read = f1.read, print(read())
# f1.read 不存在则会触发 __getattr__
--------------
# aaaaa
类 FileHandle 的构造函数中以组合的形式,将参数传给 open() 函数。获得文件句柄 self.file
,利用文件句柄我们可以对文件进行读写操作。
类中不存在 read 方法,因此 f1.read
会触发 __getattr__()
,然后利用 getattr
获得 open 函数的 read 属性(或方法),最后返回。
write 方法
import time
def write(self, line):
t = time.strftime('%Y-%m-%d %X') # 获取当前字符串时间
time.sleep(0.1)
self.file.write('%s %s' % (line, t))
# 把 write 授权给 FileHandle
f1 = FileHandle('a.txt', 'r+')
f1.write('第一行\n')
f1.write('第二行\n')
f1.write('第三行\n')
f1.seek(0) # seek 不存在,触发 __getattr__
print(f1.read())