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)

浙公网安备 33010602011771号