类与对象深度问题与解决技巧
抽象基类
概念及特点
- 抽象基类(abstract base class,ABC):抽象基类就是类里定义了纯虚成员函数的类。纯虚函数只提供了接口,并没有具体实现。抽象基类不能被实例化(不能创建对象),通常是作为基类供子类继承,子类中重写虚函数,实现具体的接口。
- 抽象基类两个特点:1.抽象基类不能被实例化 2.子类继承抽象基类必须重写指定方法
- 总的来说抽象基类就是定义各种方法而不去实现的类,任何继承自抽象基类的类必须重写该方法,否则不能实例化
- 纯虚成员函数通过使用装饰器@abstractmethod定义,继承自抽象基类的子类必须重写纯虚成员函数
应用场景
-
检查某个类中是否有某种方法
- 定义demo类,类中包含__len__方法,导入抽象基类中的sized类
-
通过查看Sized源码,在python安装目录下的lib文件夹,打开collection.abc
-
通过Sized源码,可以看出通过定义纯虚函数,判断一个对象是不是Sized类,在于是否重写了len魔法方法
-
由于上面的代码里demo类重写了len魔法方法,故demo类实例化的对象d属于Sized类
-
强制子类必须实现父类的方法
-
即通过定义纯虚函数,强制子类必须实现父类方法,否则无法实例化
-
上图可以看出,由于抽象基类demo中定义了纯虚函数__len__,而子类中没有重写__len__方法,故而无法进行实例化,我们在Demo子类重写len魔法方法,在进行实例化,查看结果
-
可以看出,Demo子类成功的进行了实例化
类与对象深度问题
如何为创建大量实例节省内存
问题描述
- 在游戏开发中,有一个玩家类Player,每有一个在线玩家,在服务器内则有一个player的实例,当在线的人数很多时,将产生大量实例(百万级),如何降低这些大量实例的内存开销?
解决方法
- 定义类的__slots__属性,声明实例中有哪些属性,关闭动态绑定
- 它的作用是阻止实例化时为类分配dict,默认情况下每个类都会有一个dict,通过__dict__访问,这个dict维护了这个实例的所有属性,由于每次实例化都会分配一个新的dict,因此存在空间浪费
- __slots__是一个元组,包含了当前能访问到的属性
- 当定义了slots之后,slots中定义的变量就变成了类的描述符,类的实例只能拥有slot中定义的变量,不能再增加新的变量
- 定义了slot之后,类就不再有dict属性
- 因此我们通过在类的定义中增加slot,减少实例化后的内存,并检查定义和不定义slot所占用的内存区别
- 首先定义两个类,一个定义slot,一个不定义
- 实例化两个类,并查看实例化一定次数之后它们所占用的内存
- 最后是两个不同类的结果
如何派生内置不可变类型并修改其实例化行为
问题描述
- 我们想自定义一种新类型的元组,对于传入的可迭代对象,我们只保留其中int类型且值大于0的元素
解决方法
- 首先只保留其中的int类型且值大于0的元素,通过一些代码进行筛选很容易实现,而我们需要考虑的是如何将这些代码添加进类的定义中
- 为了实现上述问题,需要了解一下__new__方法和__init__方法的基本知识
- __new__在创建实例对象时有python解释器自动调用,一般不用自己定义,默认调用该类的直接父类的__new__方法实例化对象,__new__方法自己定义时必须要有返回值,返回实例化出来的实例,即__init__方法中的默认参数self,如果没有返回值,则__init__方法不会执行,即没有生成self参数
- 因此实例化一个对象的步骤是先通过__new__方法返回要实例化的对象,再通过__init__方法,上图中的代码如要正常执行__init__方法中的print语句,应添加返回值,返回值为固定写法,return super().new(cls)或者父类名.new(cls)
- 因此有筛选功能的代码应该添加到__new__方法或者__init__方法中,具体要看元组是通过哪一步生成实例对象的,可以通过下面的代码查看
- 从上面的结果我们可以看出元组在new函数执行完之后就已经创建好了实例对象,因此想要解决上述问题,我们需要做的是重写__new__方法
with语句简化上下文管理器
问题描述
- with语句处理对象必须有 __enter__ 方法及 __exit__ 方法。并且 __enter__ 方法在语句体(with语句包括起来的代码块)执行之前进入运行, __exit__ 方法在语句体执行完毕退出后自动运行。那么如何自定义类使用上下文管理器。
解决方法
- 首先明确几个概念
- 上下文表达式: with open(‘juran.txt’, ‘r’) as f
- 上下文管理器:open(‘juran.txt’, ‘r’)
- 资源对象: f
- 如果想用with语句处理对象,那么对象必须有__enter__ 方法及 __exit__ 方法。
- 定义了__enter__ 方法及 __exit__ 方法之后,对象即可以通过with语句处理
- 注意__enter__函数必须返回self,类似于__new__函数
- __exit__函数的参数含义如下:
- exc_type 异常类
- exc_val 异常值
- exc_tb 追踪信息
- 具体可以通过contextlib装饰器实现简化
如何让类支持比较操作
问题描述
- 有时我们希望自定义类的实例间可以使用,<,<=,>,>=,==,!=符号进行比较,我们自定义比较的行业,例如,有一个矩形的类,比较两个矩形的实例时,比较的是他们的面积
解决方法
- 通过在类中定义具体的比较魔法方法,可以让类支持比较操作
- 一些用于比较的魔法方法如下
- 大于__gt__
- 小于__lt__
- 大于等于__ge__
- 小于等于__le__
- 接下来是具体实现代码
- 注意:当类中定义了大于魔法方法之后,自动支持小于比较,而要支持大于等于则必须定义大于等于魔法方法,同样只定义一个,就可以实现大于等于和小于等于
- 除了上述方法,我们还可以通过total_ordering装饰器完成,我们只需定义__gt__和__eq__两个魔法方法,其他的比较运算由total_ordering通过这两个魔法方法完成,具体实现如下
- 这里如果我们不止要求出矩阵的面积,还有比如圆或者其它图形,并让它们互相比较,可以通过定义一个抽象基类,定义好比较函数,并让每个子类重写求面积的函数,来实现这个需求,具体实现代码如下:
from functools import total_ordering
from _collections_abc import ABCMeta,abstractmethod
import math
@total_ordering
class shape(metaclass=ABCMeta):
def __init__(self,l,h):
pass
@abstractmethod
def area(self):
pass
def __gt__(self, other):
return self.area() > other.area()
def __eq__(self, other):
return self.area() == other.area()
class Rect(shape):
def __init__(self,l,h):
self.l=l
self.h=h
def area(self):
return self.h*self.l
class Circle(shape):
def __init__(self,r):
self.r=r
def area(self):
return self.r**2*math.pi
通过实例方法名字的字符串调用方法
问题描述
- 假设我们有三个图形类,他们都有一个获取图形面积的方法,但是方法名字不同,我们如何实现一个统一的获取面积的函数,使用每种方法名进行尝试,调用相应类的接口
解决方法
- 实现一个统一的获取面积的函数
- 假设我们有以下三种类,每个类关于面积的函数名称都不一样
- 通过定义一个统一的获取面积函数,通过getattr来实现,getattr的作用是获取当前对象中的指定方法并返回,里面的参数第一个是当前对象,第二个是需要获取的方法,以字符串表示,如果不希望获取不到指定方法时报错,可以传入一个None