第6章 面向对象编程进阶
6.1 类的定制
- 特殊属性和特殊方法
- 又称为魔法属性或魔法方法
- 协议
- 一组非强制性的特殊方法
6.1.1 常用特殊属性
__dict__
__dict__是一个属性字典- 多数数据类型都有自己的
__dict__属性 - 模块:模块中定义的函数、类、变量等构成的字典
- 函数:初始状态为空字典
- 类:普通方法、类方法、静态方法、类属性
- 对象:实例属性
1 2 | |
1 | |
1 | |
1 2 | |
1 | |
1 2 3 4 | |
1 | |
1 2 3 4 5 6 | |
1 2 | |
1 | |
- 派生类的
__dict__不会继承基类的__dict__属性,它们的__dict___是独立的
1 2 | |
1 | |
1 | |
- 一些内置数据类型的实例没有
__dict__属性 list、dict、set、tuple
1 2 | |
1 2 3 4 5 6 7 8 9 10 | |
__base__与__bases__
__base__- 当前类的基类
- 多重继承的情况下是第一个基类
__bases__- 当前类的所有基类构成的元组
__slots__
- 用于指定一组实例属性名称,实例属性的名字只能来自其中
- 类中包含
__slots__属性时 - 利用固定大小的数组来存储实例属性
- 实例不再有
__dict__属性 - 能够节约内存空间
1 2 3 4 5 6 7 8 9 10 | |
1 2 3 | |
1 | |
1 | |
1 2 3 4 5 6 7 8 9 | |
- 不在
__slots__中的属性不能被动态绑定 - 用于限制实例的动态绑定
1 2 | |
1 | |
1 | |
1 2 3 4 5 6 7 8 9 | |
6.1.2 对象运算(运算符重载)
- Python中运算符的的执行过程实际上是调用了对象的特殊方法
- 例如,
+运算符相当于__add__方法,-运算符相当于__sub__方法 - 在类定义中根据需要实现这些方法,其实例之间就能够利用运算符进行运算
1 | |
1 2 | |
1 | |
1 2 | |
1 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
1 2 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
- 常用运算符的特殊方法
| 运算符 | 特殊方法 | 作用 |
|---|---|---|
+、-、*、/、** |
__add__、__sub__、__mul__、__truediv__、__pow__ |
算术运算符 |
%、// |
__mod__、__floordiv__ |
取余、整除 |
+x、-x |
__pos__、__neg__ |
正、负 |
>、>=、<、<=、==、!= |
__gt__、__ge__、__lt__、__le__、__eq__、__ne__ |
比较运算符 |
+=、-=、*=、/= |
__iadd__、__isub__、__imul__、__idiv__ |
增强运算符 |
- 反向的特殊方法
- 当两个对象利用双目运算符时,首先检查左侧的对象是否实现了相应的特殊方法,如果有则调用左侧对象中的方法;否则,检查右侧的对象是否定义反向的特殊方法
- 所有双目运算符都有对应的反向特殊方法
__add__的反向特殊方法为__radd____sub__的反向特殊方法为__rsub__
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
1 2 | |
1 | |
1 | |
1 | |
1 2 3 4 5 6 7 8 9 | |
6.1.3 对象描述
print函数打印至输出终端__str__str(obj)- 在交互式环境中显示对象
__repr__repr(obj)
1 | |
1 | |
1 | |
1 | |
1 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
1 | |
1 | |
1 2 | |
1 | |
1 2 3 4 5 6 7 | |
1 | |
1 2 3 4 5 6 7 | |
1 | |
1 2 3 4 5 6 7 | |
6.1.4对象成员访问控制
__开头的属性具有一定的隐藏性,但并非真正的私有化- 特殊方法
__getattr__:当试图访问一个对象中不存在的属性时触发该方法;__getattribute__:当试图访问对象属性(包括不存在的属性)时;__setattr__:当试图为对象绑定属性时触发该方法;__delattr__:当试图销毁对象属性时触发该方法。
1 2 3 4 5 6 7 8 9 10 11 12 | |
1 | |
1 | |
1 | |
1 2 | |
1 2 3 4 5 6 7 | |
1 2 | |
1 2 | |
__getattribute__- 只要试图访问对象的属性就会被触发
- 类中定义了
__getattribute__方法时,__getattr__方法就会失效
1 2 3 4 5 6 | |
1 2 | |
1 | |
- 无限递归调用风险
- 在
__getattr__中试图访问对象中不存在的属性时 - 在
__getattribute__中试图访问对象中的任何属性时 - 解决办法
- 通过访问父类属性
- 使用一个代理对象
6.1.5 描述器
- 描述器是实现了
__get__、__set__或__delete__三个方法中的一个或多个的类的实例 -
描述器一般不会单独使用,最为常见的情况是将描述器作为其他类的属性
-
描述的工作原理
- 默认情况下,在访问一个对象的属性时,解释器会依次查找该对象的
__dict__属性、 所属类的__dict__属性、父类的__dict__属性......,直到找到所要访问的属性 -
当找到的属性是一个描述器时,并不会直接返回该描述器,而是会调 用描述器中的
__get__、__set__或__delete__方法 -
__get__(self, obj, type):当读取描述器中的值时触发,其中obj是描述器所属 的类的实例,type是描述器所属的类 __set__(self, obj, value):当向描述器写入值时触发-
__delete__(self, obj):当销毁描述器时触发 -
定义了
__set__或__delete__方法的对象称为数据描述器,仅定义了__get__方法 的描述器称为非数据描述器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
1 2 | |
1 | |
1 2 | |
1 | |
6.1.6 可调用对象
- 可调用对象
- 函数
- labmda表达式
- 类
- 类方法
__call__- 如果一个类中定义了特殊函数
__call__,那么它的实例也是可调用对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
1 2 3 4 5 6 7 | |
1 | |
1 2 3 4 5 6 7 8 9 10 | |
1 | |
6.1.7 容器
- 容器
- 能够持有其他对象
- 具有读取、保存、删除元素、返回其中保存的元素的数量的方法
- 容器特殊方法
__getitem__:使用[]读取保存在容器中的元素时触发;__setitem__:修改容器中的元素时触发;__delitem__:删除容器中的元素时触发;__len__:使用len函数获取容器中的元素数量时触发;__contains__:使用in或not in判断元素是否包含在容器中;__reversed__(self):当容器中元素有序时,使用reversed函数反转元素的顺序时触发;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
1 2 | |
1 | |
1 2 3 | |
1 | |
1 2 | |
1 | |
1 | |
1 | |
1 | |
1 | |
6.1.8 迭代器与可迭代对象
迭代器
- 迭代器协议
__iter__:返回一个迭代器对象,由iter函数触发__next__:返回迭代器中的下一个元素,由next函数触发StopIteration异常- 迭代器在迭代的过程中,遇到
StopIteration异常表示迭代过程结束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
1 2 | |
1 | |
1 2 | |
1 | |
可迭代对象
- 能够被迭代的对象称为可迭代对象
- 可用于
for循环、各种推导式、元组拆包、序列解包、参数分配等操作 - 迭代器是可迭代对象的一种
1 2 3 4 5 6 | |
1 2 3 | |
1 2 3 4 5 | |
ti确实不是迭代器,它能够被迭代的原因是实现了__getitem__方法- 运行过程
- Python解释器会自动调用
iter(obj) iter函数首先检查obj中是否包含__iter__方法- 如果是,则调用它并获取一个迭代器
- 如果
obj中没有__iter__方法,则进一步检查它是否包含__getitem__方法 - 如果是,则尝试按整数索引获取
obj中的元素,并基于这些元素创建出一个可迭代对象 - 如果
obj中不包含__getitem__方法,返回错误"TypeError: 'obj' object is not iterable"
- 迭代器与可迭代对象的重要区别
- 迭代器只能使用一次,经过一次迭代
next函数已经遇到StopIteration异常,迭代器不会自动重置 - 可迭代对象则可以重复被迭代,每一次迭代都会重新创建一个新的可迭代对象。
6.2 生成器
6.2.1 生成器的创建
生成器函数
yield- 在函数中返回一个数值
yield的值不是通过函数调用返回的,而是返回到迭代过程之中- 使用了
yield返回值的函数称为生成器函数 - 一个函数中一旦使用了
yield关键字,它就不再是一个普通的函数 - 即便中其中包含了
return语句,在调用的时候也不会返回值,而是返回一个生成器对象 - 生成器函数可被看作是生成器工厂
1 2 3 | |
1 2 | |
1 | |
- 生成器是一种特殊的迭代器,可利用
next函数获取yield返回的数值
1 2 3 | |
1 2 3 4 5 6 7 8 9 10 11 | |
yield语句- 函数中可以多个
yield语句或者多次执行yield语句,每次执行yield语句都会返回一个值 yield语句被执行并返回值后,生成器就被阻塞掉,不会自动继续运行,直到下一次调用next函数来激活- 当生成器函数中没有
yield语句可以被再次执行时,执行next会抛出StopIteration异常
1 2 3 4 | |
1 2 | |
1 2 3 4 5 6 7 | |
1 | |
1 2 3 4 5 6 7 | |
1 | |
1 2 3 4 5 6 7 8 9 | |
- 生成器可以像其他可迭代对象一样使用
1 2 3 | |
1 2 3 4 5 6 7 8 9 10 | |
- 惰性计算
- 生成器中并不持有全部迭代元素,每次
next执行临时生成一个元素并返回 - 原因
- 每次调用
yield语句返回值后都会被阻塞
- 每次调用
生成器推导式
- 生成器推导式与生成器函数的作用相同,都是返回一个生成器,只不过提供了一种更加便捷的语法
- 生成器函数 vs 生成器推导式
- 当生成器要实现的功能比较复杂时最好使用生成器函数
- 当功能仅通过一个表达式就能够实现时选择生成器推导式更合适
1 2 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 2 3 4 5 6 7 8 9 | |
yield from
- 为生成器函数依次返回可迭代对象中的元素,从而替代循环语句,使得代码更加简洁有效
- 包含了复杂的异常处理的工作,可用于协程之中实现复杂的功能
1 2 3 | |
1 | |
1 | |
1 | |
6.2.2 生成器与迭代器
- 将
__iter__作为一个生成器函数来使用
1 2 3 4 5 6 7 8 | |
1 2 3 | |
1 2 3 | |
6.3 类装饰器
6.3.1 修饰方法的装饰器
- 与函数装饰器相同,但需要处理方法特殊参数
self或cls
1 2 3 4 5 6 7 8 9 | |
1 2 3 4 | |
1 2 | |
1 2 3 | |
- 带参数的方法装饰器
1 2 3 4 5 6 7 8 9 10 11 | |
1 2 3 4 | |
1 2 | |
1 2 3 | |
- 在不对类或实例进行操作时,可定义即能用于函数又能用于方法的装饰器
1 2 3 4 5 6 7 8 9 10 | |
6.3.2 修饰类的装饰器
- 原理
- 在修饰类的装饰器中定义一个类,替换掉被修饰的类
- 为了保留被修饰类的原有功能,通常需要实例化一个被修饰类的对象,将该对象作为新类的属性,通过属性代理访问的方式来实现被修饰类的功能
1 2 3 4 5 6 7 8 | |
1 2 3 4 | |
1 2 | |
1 2 | |
- 更加灵活的方式
getattr(obj, prop_name)函数的作用是返回obj的名为prop_name的的属性
1 2 3 4 5 6 7 | |
6.3.3 基于类的装饰器*
- 利用类来实现装饰器
- 修饰函数
- 修饰方法
- 修饰类
修饰函数类装饰器
1 2 3 4 5 6 7 8 9 10 11 12 | |
1 2 3 | |
1 | |
1 | |
1 | |
1 | |
- 保留元信息
- 利用
wraps函数,或者功能相似的update_wrapper函数来实现
1 2 3 4 5 6 7 8 9 | |
1 2 3 | |
1 2 | |
1 | |
修饰方法的类装饰器
- 在装饰器类的
__call__方法处理特殊参数
1 2 3 4 | |
1 2 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
- 在
__call__中调用被修饰方法时,传入self即可,即self.fun(self) - 该装饰器无法再用于函数之上。
- 在装饰器中同时实现
__call__方法和__get__方法使其即能修饰方法又能修饰函数 types.MethodType的作用是将self作为方法绑定至obj实例之上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
1 2 3 4 5 6 7 8 | |
1 | |
1 2 | |
1 2 | |
1 2 3 4 5 | |
- 实现原理
- 实现
__get__方法之后,装饰器成为一个描述器 TestDec类中被修饰的method方法被替换为一个同名的描述器- 在访问
td对象的method方法时,会进一步调用描述器的__get__方法
修饰类的类装饰器
- 装饰器
__call__方法中创建一个被修饰类的实例并返回
1 2 3 4 5 6 7 | |
1 2 3 4 | |
1 2 | |
1 2 3 4 5 6 7 | |
6.4 抽象基类*
6.4.1 抽象基类的概念
- 抽象类与抽象方法
- 抽象类的主要作用有
- 提高软件的灵活性
- 提高代码规范程序
- 便于软件的设计
- 提供一类实例共性方法的统一实现
- Python中的抽象基类
- 通常不需要自定义抽象基类,大部分情况下内置的抽象基类就能够满足需要
6.4.2 抽象基类的应用
- 方法
- 派生抽象基类
- 注册虚拟子类
派生抽象基类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
1 2 | |
1 2 | |
1 | |
- 抽象基类与鸭子类型
- 抽象方法强制实现
- 能够使用
isinsance和issubclass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
1 | |
1 | |
1 2 | |
1 | |
- 某些情况下即便没有继承抽象基类,使用
isinstance也会返回True
1 2 3 4 5 6 7 8 | |
1 2 | |
1 | |
- 原因
abc.Sized抽象基类中实现了一个特殊方法__subclasshook__
1 2 3 4 5 6 | |
注册虚拟子类
- 抽象基类的
register方法或register装饰器 - 特点
- 注册虚拟子类时,Python不会检查抽象方法是否被实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
1 2 | |
1 | |
常用内置抽象基类
collections.abc模块
| 抽象基类 | 继承自 | 抽象方法 | Mixin 方法 |
|---|---|---|---|
Container |
__contains__ |
||
Hashable |
__hash__ |
||
Iterable |
__iter__ |
||
Iterator |
Iterable |
__next__ |
__iter__ |
Reversible |
Iterable |
__reversed__ |
|
Generator |
Iterator |
send, throw |
close, __iter__, __next__ |
Sized |
__len__ |
||
Callable |
__call__ |
||
Collection |
Sized, Iterable, Container |
__contains__, __iter__, __len__ |
|
Sequence |
Reversible,Collection |
__getitem__, __len__ |
__contains__, __iter__, __reversed__, index, count |
MutableSequence |
Sequence |
__getitem__, __setitem__, __delitem__, __len__, insert |
继承自 Sequence 的方法,以及 append, reverse, extend, pop, remove, __iadd__ |
ByteString |
Sequence |
__getitem__, __len__ |
继承自 Sequence 的方法 |
Set |
Collection |
__contains__, __iter__,__len__ |
__le__, __lt__, __eq__, __ne__, __gt__, __ge__, __and__, __or__,__sub__, __xor__, isdisjoint |
MutableSet |
Set |
__contains__, __iter__, __len__, add, discard |
继承自 Set 的方法以及 clear, pop, remove, __ior__, __iand__, __ixor__, __isub__ |
Mapping |
Collection |
__getitem__, __iter__, __len__ |
__contains__, keys, items, values, get, __eq__, and __ne__ |
MutableMapping |
Mapping |
__getitem__, __setitem__, __delitem__, __iter__, __len__ |
继承自 Mapping 的方法以及 pop, popitem, clear, update,和 setdefault |
MappingView |
Sized |
__len__ |
|
ItemsView |
MappingView , Set |
__contains__, __iter__ |
|
KeysView |
MappingView , Set |
__contains__, __iter__ |
|
ValuesView |
MappingView , Collection |
__contains__, __iter__ |
|
Awaitable |
__await__ |
||
Coroutine |
Awaitable |
send, throw |
close |
AsyncIterable |
__aiter__ |
||
AsyncIterator |
AsyncIterable |
__anext__ |
__aiter__ |
AsyncGenerator |
AsyncIterator |
asend, athrow |
aclose, __aiter__, __anext__ |
6.4.3 自定义抽象基类
- 自定义抽象基类需要继承
abc.ABC类 - 使用
abc.abstractmethod装饰器将一个方法声明为抽象方法 abstractmethod装饰器也可以和其他装饰器叠加使用,如staticmethod、classmethod,不过abstractmethod必须是最内层装饰器
1 2 3 4 5 6 7 8 9 10 | |
6.5 元类*
6.5.1 Python类的特征
类的一等对象特征
- Python中的类
- 可以被赋值给变量
- 作为参数传递给函数
- 可以作为函数的返回值
- 能够在程序运行期间动态创建
1 2 3 4 5 6 7 | |
1 2 3 | |
1 | |
1 2 3 | |
1 | |
1 | |
1 | |
类的类型
- 即然Python中的类也是对象,那么它们与其他的对象类似,也需要有一个“类”来描述和创建
- 元类
- 用于创建类的“类”称为元类
- 类是元类的实例,元类是比类更高一级的抽象
1 2 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
- 不只是自定义的类,如果没有指定元类,默认情况下Python中所有类的类型都是
type
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
类的动态创建
get_class函数每一次调用它都会动态地创建出一个新的类- 使用
type函数能够更加灵活地动态创建类,它需要三个参数 name:字符串,用于指定类的名字;bases:元组,用于指定基类,当为空元组时表示使用默认基类object;dict:字典,用于指定类的属性和方法,空字典表示没有属性或方法。
1 2 3 4 | |
1 | |
1 | |
1 | |
- 静态方法与类方法
- 只需要像普通的类定义中那样使用
classmethod或staticmethod装饰器修饰函数属性即可 - python中
type的作用 - 作为函数来判断数据的类型
- 是所有元类的默认基类,所有的元类都必须派生自
type - 可以用于动态创建类
6.5.2 元类的定义与使用
自定义元类
- Python中所有的元类都必须是
type的派生类 - 在类定义的时候,如果指定父类为
type或者基它元类,那么这个类就是一个元类 - 元类的定义与普通的类基本一致,可以使用一些特殊方法对元类进行定制
__new__方法- 四个参数
cls、name、bases、dict
1 2 3 4 5 6 7 8 9 | |
使用元类
1 2 3 | |
1 2 | |
1 2 3 4 5 6 7 8 9 10 | |
1 | |
1 | |
类的创建过程
- 类创建过程
- Python解释器解析类的代码
- 将类的名称、基类元组、属性字典传递给元类的
__new__方法 __new__方法根据传入的参数调用type来创建类- 元类的使用
- 当前类指定了元类,则用它来创建类
- 如果当前类没有指定元类则进一步遍历检查基类中是否指定元类
- 如果基类中也没有指定元类,则使用默认的
type来创建类
6.5.3 元类的应用实例
应用场景
- 元类是Python中最为复杂的内容之一,而且它的部分功能也可以使用装饰器或者类的定制来实现
- 在绝大多数的Python编程任务中不需要使用元类
- 元类最常用的应用场景是构建应用程序的API或编程框架
利用元类实现ORM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
1 2 3 4 5 6 7 | |
1 2 | |
6.6 对象序列化*
- 序列化
- 对象序列化(Serialization)也称为对象持久化,是指将内存中的对象转换为数据流, 能够存储在文件之中或者通过网络进行传输
- 反序列化
- 对象反序列化是指将二进制字节 数据流恢复重建为 Python 对象
6.6.1 pickle
- 对象序列化和反序列化最常用的工具。常用函数包括:
dump(obj, file, protocol=None):将对象obj序列化为二进制数据,并写入打开的文件对象file之中load(file):从打开的文件对象file中加载序列化对象,反序列化重建对象dumps(obj, protocol=None):将对象obj序列化为二进制数据-
loads(data):对序列化之后的二进制数据进行反序列化操作,重建对象 -
绝大多数Python数据类型都可以被序列化和反序列化,包括
None、 布尔类型(True和False)、字符串(str)、字节串(byte)、字节数组(bytearray)- 全局函数和类
__dict__属性或__getstate__方法返回值可以被可被序列化的对象- 元素都为可序列化对象的列表、元组、集合和字典
一般数据类型的序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
函数、类和实例的序列化
- 在序列化时,只有函数名和类名被序列化,而函数体及类体(包括类数据)不会被序列化
-
在对函数和类进行反序列化的时候,它们的定义必须是可见的
-
函数和类的序列化
1 2 3 4 5 6 7 | |
fun_pkl = pickle.dumps(test_fun) class_pkl = pickle.dumps(TestClass) fun = pickle.loads(fun_pkl) fun()
1 2 3 4 5 | |
1 2 3 4 5 6 7 8 9 10 11 | |
1 | |
1 2 3 4 5 6 7 8 9 | |
实例的序列化
- 实例在序列化的时候,只会保存实例中的属性数据,实例所属的类同样不会被序列化
- 在反序列化时,类也必须也是可见的
- 反序列化时不会调用
__init__方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
6.6.2 copyreg
- copyreg的作用是注册能够在序列化和反序列化过程中被调用的函数,用于控制序列化和反序列化的过程
- 在实际应用中往往会遇到软件升级的情况。升级之后,加载数据时可能会遇到旧版软件的反序列化数据,可能会出现不兼容的情况。
copyreg模块通过为目标类型注册约函数和重构函数来控制反序列化过程,从而实现版本兼容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
6.6.3 shelve
- shelve模块是对pickle模块的封装,提供了更加便捷的序列化和反序列化功能
- shelve模块中的open函数会打开序列化数据文件并返回一个特殊的字典,该字典中包含了保存在文件中的序列化数据
writeback参数用于控制是否可写入新的数据至序列化文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
1 2 3 4 | |
本页面的全部内容在 生信资料 bio.0594codes.cn 和 莆田青少年编程俱乐部 0594codes.cn 协议之条款下提供,附加条款亦可能应用