# Python 高级应用 第七章

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

要求:

  1. 可以对实例变量名指定类型
  2. 赋予不正确的类型时抛出异常

==解决方案== 使用描述符来实现需要类型检查的属性:

分别实现__ 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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容