python 实用编程技巧 —— 类与对象深度问题与解决技巧
如何派生内置不可变类型并修其改实例化行为
实际案例
我们想要自定义一种新类型的元组, 对于传入的可迭代对象, 我们只保留作其中int类型且值大于0的元素, 例如:
IntTuple([1, -1, 'abc', 6, ['x', 'y'], 3]) => (1, 6, 3)
如何继承 内置 tuple 实现 IntTuple?
解决方案
重构 __new__ 方法
class IntTuple(tuple): def __new__(cls,iterable): g = (x for x in iterable if isinstance(x,int) and x>0) return super(IntTuple,cls).__new__(cls,g) t = IntTuple([1,-1,'abc',6,['x','y'],3]) print(t)
如何为创建大量实例节省内存
实际案例
在某网络游戏中, 定义了玩家类 Player(id, name, level,...)
每有一个在线玩家, 在服务器程序内则有一个 Player 的实例,
当在线人数很多时, 将产生大量实例。 (如百万级)
如何降低这些大量实例的内存开销?
解决方案
- 定义类的 slots 属性,声明实例有哪些属性(关闭动态绑定)
- __slots__属性提前定义好了属性,不会再变,没有了__dict__属性,也就没有了动态绑定功能,类似于C语言的结构体
class Player1: def __init__(self, uid, name, level): self.uid = uid self.name = name self.level = level class Player2: __slots__ = ['uid', 'name', 'level'] def __init__(self, uid, name, level): self.uid = uid self.name = name self.level = level p1 = Player1('0001', 'Jim', '20') p2 = Player2('0001', 'Jim', '20') set(dir(p1)) - set(dir(p2)) # 发现p1属性 多于p2 # 多了 __dict__, 和__weakref__两个属性 # 主要在__dict__ 属性上 会多消耗内存 # __dict__ 动态维护 实例属性 像self.uid self.name 这些都是它维护的, # __dict__ 可以动态添加属性 如 p1.newadd = 1 会自动在p1.__dict__的字典中加入 'newadd': 1 import sys sys.getsizeof(p1.__dict__) # 864 sys.getsizeof(p1.name) # 52 sys.getsizeof(p1.level) # 28 sys.getsizeof(p1.uid) # 53 # 可以看到 __dict__浪费了一部分内存, 如果实例比较少, 问题不大, 但实例非常多, 会非常浪费内存 # 如果使用了__slots__ 就提前确定的内存, 无法对实例动态添加属性了 像 p2.newadd = 1 就会报错,无法实现。 # 原有属性不受影响 p2.name = "newname" 都是可行的
如何创建可管理的对象属性
实际案例
在面对对象编程中, 我们把方法(函数)看作对象的接口。 直接访问对象的属性是不安全的, 或者设计上不够灵活。 但是使用调用方法在形式上不如访问属性简洁 circle.get_radius() circle.set_radius(5.0) # 繁 circle.radius circle.radius = 5.0 # 简
解决方案
- 使用property
import math class Circle: ''' 分别使用property的两种使用方法, 实现 面积s 和 半径r 的操作 ''' def __init__(self, radius): self.radius = radius def get_radius(self): return round(self.radius, 1) def set_radius(self, radius): if not isinstance(radius, (int, float)): raise TypeError('wronge type') self.radius = radius # property用法一 # @property 和 @函数名.setter装饰器 @property def S(self): return self.radius ** 2 * math.pi @S.setter def S(self, s): self.radius = math.sqrt(s / math.pi) # property 用法二 # 参数分别是 属性的访问 属性的赋值 属性的删除 都是函数参数 R = property(get_radius, set_radius) c = Circle(5.712) c.S = 99.88 print(c.S) # 99.880000000 print(c.R) # 5.6 print(c.get_radius()) # 5.6
如何让类支持比较操作
解决方案
- 利用 __lt__小于,__eq__等于等魔法方法
- 比较的方法写在抽象类中, 让其他类继承即可减少 代码编写量
- 使用total_ordering装饰器装饰抽象基类来简化实现过程
from functools import total_ordering from abc import ABCMeta, abstractclassmethod @total_ordering # 修饰后, 只要实现__eq__ 和 剩下的比较操作中 随便实现一个, 就能实现所有比较 class Shape(metaclass=ABCMeta): # 定义抽象类 @abstractclassmethod # 定义抽象方法 def area(self): pass def __lt__(self, obj): print('__lt__', self, obj) return self.area() < obj.area() def __eq__(self, obj): return self.area() == obj.area() class Rect(Shape): def __init__(self, w, h): self.w = w self.h = h def area(self): return self.w * self.h def __str__(self): return 'Rect:(%s, %s)' % (self.w, self.h) import math class Circle(Shape): def __init__(self, r): self.r = r def area(self): return self.r ** 2 * math.pi rect1 = Rect(6, 9) # 54 rect2 = Rect(7, 8) # 56 c = Circle(8) # 201.06 print(rect1 < c) # True print(c > rect2) # True
如何使用描述符对实例属性做类型检查
解决方案
- 使用 __ dict __ 的特性
- 综合使用 __ se t__, __ get __, __ dele
- __set__(self, instance, value) 中的 instance 指的是实例对象,在这里是 Person 实例
class Attr: def __init__(self, key, type_): self.key = key self.type_ = type_ def __set__(self, instance, value): print('in __set__') if not isinstance(value, self.type_): raise TypeError('must be %s' % self.type_) instance.__dict__[self.key] = value def __get__(self, instance, cls): print('in __get__', instance, cls) return instance.__dict__[self.key] def __delete__(self, instance): print('in __del__', instance) del instance.__dict__[self.key] class Person: name = Attr('name', str) age = Attr('age', int) p = Person() p.name = 'cannon' # 会调用__set__方法 p.age = '26' # '32'不是int, 会报错
如何在环状数据结构中管理内存
实际案例
在python中, 垃圾回收器通过引用计数来回收垃圾对象,但在某些环状数据结构中(树, 图...), 存在对象间的循环引用, 比如树的父节点引用子节点, 子节点引用父节点。
此时同时 del掉引用父子节点, 两个对象不能被立即回收( 引用计算无法变为0) 如何解决此类的内存管理问题?
解决方案
- 使用弱引用 (不会增加引用计数的引用)
- 使用标准库weakref.ref() 创建弱引用
- 对于链表, 可以右node引用计数为1, 左引用计数为0
- 下面例子中 如果left是弱引用, 要得到引用就得head.left() , 为了不要(), 使用property
简单的测试小案例
import sys class A(object): def __del__(self): #析构函数,在垃圾回收器回收变量时会调用析构函数 print('in __del__') if __name__ == '__main__': a = A() print(sys.getrefcount(a)) # 打印a的循环引用计数 a1 = a print(sys.getrefcount(a)) # 打印a的循环引用计数 del a1 print(sys.getrefcount(a)) # 打印a的循环引用计数 a = 5 # 将a赋值 时,a的引用计数再减1,就不再使用了,此时垃圾回收器将回收这个变量
输出结果如下
/usr/bin/python3.5 /home/python/Desktop/zhang/import_csv/import_csv.py
2
3
2
in __del__
Process finished with exit code 0
弱引用的使用
- Weakref,可以创建能访问对象但不增加引用计数的对象。当这个对象存在时返回这个对象的引用,当这个对象不存在时返回None
简单实例
import sys import weakref class A(object): def __del__(self): #析构函数,在垃圾回收器回收变量时会调用析构函数 print('in __del__') if __name__ == '__main__': a = A() print(sys.getrefcount(a)) # 打印 a 的循环引用计数 # 创建a的弱引用对象 a_wref = weakref.ref(a) print(sys.getrefcount(a)) # 此时a的循环引用计数,数量不变 a2 = a_wref() # 将弱引用对象赋给a2 print(sys.getrefcount(a)) # 此时的引用计数增加了 print(a2 is a) print(sys.getrefcount(a2)) # 查看a2的循环引用计数 # 删除a和a2 del a2 del a print(a_wref() is None) #此时弱引用对象已回收,返回None
输出结果如下
/usr/bin/python3.5 /home/python/Desktop/zhang/import_csv/import_csv.py
2
2
3
True
3
in __del__
True
Process finished with exit code 0
键表的节点和数据
class Data(object): def __init__(self,value,node): #定义数据的值,和属于的结点 self.node = node self.value = value def __str__(self): return "%s 's data,value is %s" %(self.node,self.value) def __del__(self): print('in Data.__del__') class Node(object): def __init__(self,value): #定义一个结点,并赋初值数据。 self.data = Data(value,self) #结点的数据是Data()类的对象, def __del__(self): print ('in Node.__del__') node = Node(100) del node print ('end')
输出结果如下
/usr/bin/python3.5 /home/python/Desktop/zhang/import_csv/import_csv.py
end
in Node.__del__
in Data.__del__
先输出的是 end 说明循环引用没有被释放
使用 弱引用进行改写
import weakref class Data(object): def __init__(self,value,node): self.node = weakref.ref(node) #创建结果的弱引用对象 self.value = value def __str__(self): return "%s 's data,value is %s" %(self.node(),self.value) def __del__(self): print('in Data.__del__') class Node(object): def __init__(self,value): self.data = Data(value,self) def __del__(self): print ('in Node.__del__') node = Node(100) del node print ('end')
输出结果如下
/usr/bin/python3.5 /home/python/Desktop/zhang/import_csv/2-1.py
in Node.__del__
in Data.__del__
end
如何通过方法名字的字符串调用方法
实际案例
在某项目中, 我们的代码使用了三个不同库中的图形类:
Circle, Triangle, Rectangle
它们都有一个获取图形面积的接口(方法), 但接口名字不同。我们可以实现一个统一的获取面积的函数,
使用每种方法名进行尝试, 调用相应类的接口
解决方案
- lib1
# lib1 class Circle: def __init__(self, r): self.r = r def area(self): return relf.r ** 2 ** 3.14159
- lib2
# lib2 class Triangle: def __init__(self, a, b, c): self.a, self.b, self.c = a, b, c def get_area(self): a, b, c = self.a, self.b, self.c p = (a + b + c) / 2 return (p * (p - a) * (p - b) * (p - c)) ** 0.5
- lib3
# lib3 class Rectangle: def __init__(self, a, b): self.a, self.b = a, b def getArea(self): return self.a * self.b
- 对Circle, Rectangel, Triangle 求面积
- 方法一: 使用内置函数getattr, 通过名字获取方法对象, 然后调用
- 方法二: 使用标准库operator 下的methodcaller函数调用
from lib1 import Circle from lib2 import Triangle from lib3 import Rectangle from operator import methodcaller def get_area(shape, method_name = ['area', 'get_area', 'getArea']): # 方法的名字都先放入一个列表中 for name in method_name: if hasattr(shape, name): return methodcaller(name)(shape) # 或者 # f = getattr(shape, name, None) # if f: # return f() shape1 = Circle(1) shape2 = Triangle(3, 4, 5) shape3 = Rectangle(4, 6) shape_list = [shape1, shape2, shape3] # 获得面积列表 area_list = list(map(get_area, shape_list)) print(area_list)