Python 高级应用 第七章
7-1 如何派生内置不可变类型并修改器实例化行为
==实际案例== 我们想自定义一种新型的元组,对于传入的可迭代对象,我们只保留作我中int 类型且大于0的元素,例如:
IntTuple([1,-1,'abc',6,['x','y'],3]) => (1,6,3)
要求IntTuple是内置tuple的子类,如何实现
class IntTuple(tuple):
def __int__(self, iterable):
super(IntTuple, self).__int__(iterable)
#当前IntTuple和Tuple行为是一致的,因为我们没有对其进行任何的修改
t=IntTuple([1,-1,'abc',6,['x','y'],3])
print(t)
class IntTuple(tuple):
def __int__(self, iterable):
super(IntTuple,self).__int__((1,6,3)) #自己对其进行改变
#self是Tuple的一个示例对象,没有办法对其进行改变
==解决方案== 定义类IntTuple继承内置tuple, 并实现__ new__ ,修改实例化行为
class IntTuple(tuple):
def __new__(cls,iterable): #self是由__new__方法创建
g = (x for x in iterable if isinstance(x,int) and x>0)
return super(IntTuple, cls).__new__(cls,g)
def __int__(self,iterable):
super(IntTuple, self).__init__(iterable)
t=IntTuple([1,-1,'abc',6,['x','y'],3])
print(t)
2
7-2如何创建大量实例节省内存
==实际案例== 某网络游戏玩家类Player(id, name, status,.....),每当有一个玩家,在服务器程序内就有一个Player 实例,当在线人数很多的时候,将产生大量的实例(如百万级)
如何降低这些大量实例的内存开销
==解决方案== 定义类的__ slot__属性,它是用来声明实例属性名字的列表
#定义了两个player的类,并存贮在e.py中
class Player(object):
def __int__(self,uid,name,status=0, levels=1):
self.uid=uid
self.name=name
self.stat=status
self.level=level
class Player2(object):
__slots__=['uid','name','stat','level'] #差异,slots定义了Player2 有哪些属性,因此无法随意添加属性,使用的内存要更小
def __init__(self, uid, name, status=0, levels=1):
self.uid=uid
self.name=name
self.stat=status
self.level=level
下面我们看下这两个类之间有什么不同
from e import Player, Player2 #从e.py中导出这两个类
p1 = Player('0001','Jim') #创建player的实例
p2 = Player2('0001','Jim') #创建Player2的实例
#p1比p2是用的内存多,那么一定是p1比p2多了某些属性
dir(p1) #检查P1具有哪些属性
dir(p2) #检查p2具有哪些属性
set(dir(p1)) - set(dir(p2)) #将p1和P2的属性放到集合中,然后在做集合的差集
#得到 set['__dict__', '__weakref__'] #这就是p1比p2多出来的属性
p1.__dict__ #返回的是一个字典,在内部我们得到了刚才定义的这些属性,这便于我们动态绑定属性
p1.x #首先p1是没有x这个属性的,但是我们可以自己定义
p1.x = 1223 #这样就为p1重新添加了一个属性,,之后用p1.__dict__可以发现这个属性被添加到了字典中。
p1.__dict__['y'] =99 #直接在字典中添加属性
p1.y #就可以访问y这个属性了
#同样的道理,也可以动态的解除:
del p1.__dict__['x'] #这样x属性就没有了
#这样的动态绑定属性的方法是非常占用内存的
import sys
sys.getsizeof(p1.__dict__) #查看该属性的内存占用情况
#使用__slots__来关闭__dict__这个属性,__slots__能够首先声明出该类的属性,之后就不能在向其中添加属性
p2.x #无法添加额外的属性了
7-3 如何让对象支持上下文管理
我们经常在操作文件的时候使用上下文管理
with open('demo.txt','w') as f:
f.write('duanshuemg')
f.writelines(['zda\n','132\n'])
#f,close() #使用了with之后就不在需要使用f.close() 来关闭文件了,这一切都可以交给上下文管理器,自动完成
==实际案例== 我们实现了一个telnet客户端的TelnetClient,请使用实例的stat()方法启动客户端与服务器交互,交互完毕后需要调用cleanup() 方法,关闭已连接的,socket,以及将操作历史记录写入到文件中并关闭。
能否rangTelnetClient的实例支持上下文管理协议,从而代替手工调用的cleanup()方法。
==
from telnetlib import Telnet
from sys import stdin,stdout
from collections import deque
class TelnetClient(object):
def __init__(self,addr, port=23):
self.addr=addr
self.port=port
self.tn=None
def start(self):
self.tn=Telnet(self.addr, self.port)
self.history=deque() #队列
#usr
t = self.tn.read_until('login:')
stdout.write(t)
user=stdin.readline()
self.tn.write(user)
# passwards
t=self.tn.read_until('password')
if t.startswith(user[:-1]):t=t[len(user) + 1:]
stdout.write(t)
self.tn.write(stdin.readline())
#进入服务器使用shell命令于服务器进行交互
t=self.tn.read_until('$ ')
stdout.write(t)
while True:
uinput=stdin.readline()
if not uinput:
break
self.history.append(uinput)
self.tn.write(uinput)
t = self.tn.read.until('$ ')
stdout.write(t[len(uinput) + 1:])
def cleanup(self):
self.tn.close()
self.tn=None
with open(self.addr + '_history.txt', 'w') as f:
f.writelines(self.history)
client=TelnetClient('172.18.78.62')
print('\tstart...')
client.start()
print('\nclose')
client.cleanup()
进行改进,不用cleanup,而是直接使用with语句来进行清除
==解决方案== 实现上下文管理协议,需定义实例的__ enter__ , __ exit__ 方法,它们分别在with开始和结束的时候被调用
class TelnetClient(object):
def __init__(self,addr, port=23):
self.addr=addr
self.port=port
self.tn=None
def start(self):
self.tn=Telnet(self.addr, self.port)
self.history=deque() #队列
#usr
t = self.tn.read_until('login:')
stdout.write(t)
user=stdin.readline()
self.tn.write(user)
# passwards
t=self.tn.read_until('password')
if t.startswith(user[:-1]):t=t[len(user) + 1:]
stdout.write(t)
self.tn.write(stdin.readline())
#进入服务器使用shell命令于服务器进行交互
t=self.tn.read_until('$ ')
stdout.write(t)
while True:
uinput=stdin.readline()
if not uinput:
break
self.history.append(uinput)
self.tn.write(uinput)
t = self.tn.read.until('$ ')
stdout.write(t[len(uinput) + 1:])
def __enter__(self):
self.tn=Telnet(self.addr, self.port)
sefl.history = deque()
return self
def __exit__(self,exc_type, exc_val, exc_tb):
self.tn.close()
self.tn = None
with open(self.addr + 'history.txt', 'w') as f:
f.writelines(self.history)
with TelnetClient('127.0.0.1') as client:
client.start()
7-4如何创建可管理的对象属性
==实际案例== 在面向对象编程中,我们把方法(函数)看做是对象的接口,直接访问对象的属性可能是不安全的,或者涉及上不够灵活,但是可以使用调用方法在形式上不如访问属性简洁
circle.getRadius()
circle.getRadius(5.0)
circle.radius
circle.radius=5.0 #直接访问是不安全的
from math import pi
class Circle(object):
def __init__(self, radius):
self.radius = radius
def getRadius(self):
return self.radius
def getRadius(self, value):
if not isinstance(value,(int, long, float)):
raise ValueError('wrong type.')
self.radius = float(value)
def getArea(self):
return self.radius **2** pi
c= Circle(3.2)
c.radius #返回值为3.2, 没什么问题
c.radius='ac' #重新给半径赋值的时候,即使是使用了非数字来进行赋值,也不会报错,逻辑出错,运行没错
c.setRadius('ac') 这样就会出现报错,从而能够保证输入的半径值为数字
#如果要对半径的值保留两位小数,我们就可以这样实现,首先在getRadius中对返回值进行改变
return round(self.radius, 2) #就可以实现对半径保留两位小数
c.getRadius()
==解决方案== 使用property函数为类创建可管理属性, fget/fset/fdel 对应相应的属性访问
from math import pi
class Circle(object):
def __init__(self, radius):
self.radius = radius
def getRadius(self):
return self.radius
def getRadius(self, value):
if not isinstance(value,(int, long, float)):
raise ValueError('wrong type.')
self.radius = float(value)
def getArea(self):
return self.radius **2** pi
R = property(getRadius, setRadius)
c=Circle(3.2)
c.R #调用的是R的第一个参数 getRadius,
c.R = 5.0 #此时调用的是第二个参数,也就是setRadius
c.R #此时的半径已经变成了5.0
c.R = 'ac' #可以排除错误的值
7-5 如何让类支持比较操作
==实际案例== 有时我们希望自定义的类,实例间可以使用<, <=, >, >=, ==, !=符号进行比较,我们自定义比较的行为,例如,有一个矩形的类, 我们希望比较两个矩形的实例时,比较的是他们的面积
class Rectangle:
def __init__(slef, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
#运算符的承载
def __lt__(self, obj):
print('in__lt__') #被调用了
return self.area() < obj.area()
rect1 = Rectangle(5,3)
rect2 = Rectangle(4,4)
rect1 > rect2 # => rect1.area() > rect2.area()
#上面调用的实际上是 rect1.__lt__(rect2) 左边的对参数对__ lt__进行调用
print(r1 < r2)
==解决方案== 比较运算符重载, 需要实现一下方法:
__ it__ , __ le__ , __ gt __ , __ ge __ , __ eg __ , __ ne __
使用标准库下的functools下的类装饰器, total_ordering 可以简化此过程
from functools import total_ordering
@total_ordering
class Rectangle:
def __init__(slef, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
#我们只用定义两个运算符就行,例如这里我们定义一个小于一个等于
def __lt__(self, obj):
print('in __lt__')
return self.area() < obj.area()
def __eq__(self, obj):
print('in __eq__')
return self.area() == obj.area()
#可以帮我们定义掐的函数:
def __le__(self,obj):
return self.area < obj.area or self.area == obj.area #这个操作对应的上面这两种方法
def __gt__(self,obj):
return not( self.area < obj.area or self.area == obj.area)
#当不是同一种类型的图形也可以进行比较,在这里我们再定义一个圆的类
class Circle(object):
def __init__(self,r):
self.r=r
def area(self):
return self.r **2 * 3.14
r1 = Rectangle(3,4)
c1 = Circle(3)
print(r1 <= c1)
我们需要在所有的图形中实现这些运算符重载函数,然后在人为定义一个接口
from functools import total_ordering
from abc import ABCMeta, abstractmethod
@total_ordering
class Shape(object):
@abstractmethod # 用装饰器将其装饰为抽象接口,它的子类都是集成这个方法
def area(self):
pass
def __lt__(self, obj):
print('in __lt__')
if not isinstance(obj,Shape):
raise TypeError('obj not Shape')
return self.area() < obj.area()
def __eq__(self,obj):
print('in __eq__')
if not isinstance(obj,Shape):
raise TypeError('obj is not Shape')
return self.area() == obj.area()
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return self.r ** 2 * 3.14
7-6 如何使用描述符对实例属性做类型检查
==实际案例== 在某项目中,我们实现了一些类,并希望能够像静态类型语言那样(c, c++, java) 对它们的实例属性做类型检查
P =Person()
p.name = 'Bob' #必须是str
p.age = 18 # 必须是int
p.height = 1.83 #必须是float
要求:
- 可以对实例变量名指定类型
- 赋予不正确的类型时抛出异常
==解决方案== 使用描述符来实现需要类型检查的属性:
分别实现__ get__ , __ set__ , __ delete __ 方法,在 __ set__ 内使用isinstance函数做类型检查
#描述非就是包含get, set或者delete中任何一个的类
class Descriptor(object):
def __get__(self, instance, cls):
print('in __get__', instance, cls)
return instance.__dict__['x']
def __set__(self, instance, value):
print('in __set__')
instance.__dict__['x'] = value
def __delete__(self, instance):
print('in __del__')
class A(object):
x = Descriptor()
a = A() # 我们创建了一个示例A,但是x并不是实例A的一个属性
a.x = 5
print a.__dict__ #字典是空的说明x不是A的一个属性
现在我们将描述符改成属性
class Attr(object):
def __init__(self, name, type_): #这里因为type是内置的函数,所以在后面加_
self.name = name
self.type_ = type_
def __get__(self, instance, cls):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.type_):
raise TypeError('expected an %s' % serfff.type_)
instance.__dict__[self.name] =value
def __delete(self, instance):
del instance.__dict__[self.name]
class Person(object):
name = Attr('name', str)
age = Attr('age', int)
height = Attr('height', float)
p = Person()
p.name='Bob'
print(p.name)
7-7 如何在环状数据结构中管理内存
==实际案例== 在python中,垃圾回收器通过引用计数来回收垃圾对象,但是某些环状数据结构(树,图...),存在对象间的循环引用,比如树的父节点引用子节点,子节点也同时引用父节点,此时同时del掉引用父子节点,两个对象不能被立即回收。
如何解决此类问题
class A(object):
def __del__(self):
print 'in A.__del__'
a = A() #创建实例
import sys
sys.getrefcount(a) #查看这个实例的引用计数
#它返回的结果总是比实际多一个,因为函数调用的时候也会被调用一个参数
sys.getrefcount(a) -1 #这样可以得到实际上的参数
#记住,当引用次数为0的时候,该变量就会被进行回收
a2 =a #此时被引用了两次,引用计数为2
del a2 #引用计数减一
sys.getrefcount(a)
a = 5 #当重新对a进行赋值的时候,引用次数又减少一次,析构函数A.__del__被调用
示例:
其中Data 和Node相互引用,Node自身的这个对象传给owner, self.data引用Data, self.owner 医用Node。对于这样的循环引用不能够立即被回收,可能会在将来的某个时间点被进行回收,
class Data(object):
def __init__(self, value, owner):
self.owner = owner
self.value = value
def __str__(self):
print 'in Data.__del__'
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
raw_input('wait...')
#强制回收
import gc
gc.collect() #因为我们在上面的类中定义了析构函数__del__,所以也不能用这种方法进行强制回收
==解决方案== 使用标准库weakref,它可以创建一种能够访问对象但是不增加引用计数的对象
a =A()
sys.getrefcount(a)
import waekref
a_wref = weakref.ref(a) #创建一个a的弱引用
a2 = a_wref() #a_wref的用法就可以是像函数一样
a is a2 # 查看a是否是a2
sys.getrefcount(a) -1
del a
del a2 #此时被进行回收,调用析构函数 A.__del__
a_wref()
a_wref() #现在就没有返回值,相当于None
改写刚才的循环引用,引入循环引用
import weakref
class Data(object):
def __init__(self, value, owner):
self.owner = weakref.ref(owner) #不会增加引用计数
self.value = value
def __str__(self):
return ''%$' $ data, value is %$ ' % (self.owner(),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 #增加弱引用之后就可以完成变量清理
raw_input('wait...')
7-8 如何通过实例方法名字的字符串调用方法
==实际案例== 某项目中,我们的代码使用了三个不同库中的图形类:
Circle, Triangle, Rectangle
它们都有获取图形面积的接口(方法),但接口名字不同,我们可以进一步实现一个统一的截取面积的函数,使用每种方法名进行尝试,调用相应类的接口
from lib1 import Circle
from lib2 import Triangle
from lib3 import Rectangle
def getArea(shape):
pass
shape1 = Circle(2)
shape2 = Triangle(3,4,5)
shape3 = Rectangle(6,4)
shapes=[shape1, shape2, shape3]
print map(getArea, shapes)
# lib1.py
class Circle(object):
def __init__(self,r):
self.r = r
def area(self):
return self.r **2 *3.14
#lib2.py
class Triangle(object):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def getArea(self):
a,b,c = self.a, self.b, self.c
p = (a+b+c)/2
area =(p*(p-a) * (p-b) * (p-c)) **0.5
return area
# lib3.py
class Rectanle(object):
def __init__(self,w,h):
self.w = w
self.h = h
def get_area(self):
return self.w * self.h
在三个对象中,图形面积的接口函数都不一样,所以用户很难获得,
==解决方法==
方法一: 使用内置函数getattr, 通过名字在实例上获取方法对象,然后在对其进行调用
from lib1 import Circle
from lib2 import Triangle
from lib3 import Rectangle
def getArea(shape):
for name in('area','getArea','get_area'):
f = getattr(shape, name, None)
if f:
return f()
shape1=Circle(2)
shape2=Triangle(3,4,5)
shape3=Rectangle(6,4)
shapes = [shape1, shape2, shape3]
print map(getArea, shapes)
方法二: 使用标准库operator 下的methodcaller函数来进行调用
from operator import methodcaller
s = 'abc123abc456'
s.find('abc',4) #从字符串s的第四位上查找abc
methodcaller('find', 'abc',4) #第一个参数是用到的函数,第二个参数是查找的字符串,第三个参数是其实的位置,它可以进行调用
methodcaller('find','abc',4)(s)
具体实现
shape1 = Circle(2)
shape2 = Triangle(3,4,5)
shape3 = Rectangle(6,4)
shapes = [shape1, shape2, shape3]
from operator import methodcaller
names=['area','getArea','get_area']
for name,shape in zip(names, shapes):
area= methodcaller(name)(shape)
print(area)