python进阶:第六章(类与对象深度技术进阶)

问题一:如何派生内置不可变类型并修改其实例化行为?

问题内容:
自定义一种新的元组,对于传入的可迭代对象,我们只保留其中int类型且值大于0的元素,例如:IntTuple([1,-1,'abc',6,['x','y'],3) =>(1,6,3) 并且要求IntTuple是内置tupe的子类,如何实现?

实现方案:
定义类IntTuple继承内置tuple,并实现new,实现实例化行为。
类中的self是由new方法定义,以及实例化行为。

问题二:如何为创建大量实例节省内存?

问题内容:
某游戏中,定义了玩家类Player(id,name,status,......)每有一个在线玩家,在服务器程序内则有一个Player的实例,当在线人数很多时,将产生大量实例。(如百万级)
如何降低这些大量实例的内存开销?

解决方案:
定义类的slots属性,它是用来声明实例属性名字的列表。

下面我们定义两个类:

In [1]: class Player(object):
   ...:     def __init__(self,uid,name,status=0,level=1):
   ...:         self.uid = uid
   ...:         self.name = name
   ...:         self.stat = status
   ...:         self.level = level
   ...:

In [2]: class Player2(object):
   ...:     __slots__ = ['uid','name','stat','level']
   ...:     def __init__(self,uid,name,status=0,level=1):
   ...:         self.uid = uid
   ...:         self.name = name
   ...:         self.stat = status
   ...:         self.level = level
   ...:

下面我们看下为什么Player2的实例比Player的实例占的内存小。

In [4]: p1 = Player('0001','Jim')

In [5]: p2 = Player2('0001','Jim')

In [6]: set(dir(p1)) -set(dir(p2))
Out[6]: {'__dict__', '__weakref__'}

我们发现p1比p2多了两个属性,其中的weakref并不占用多少内存,真正占内存的是dict

In [7]: p1.__dict__
Out[7]: {'level': 1, 'name': 'Jim', 'stat': 0, 'uid': '0001'}

我们发现dict是一个字典,是为了实现动态绑定的字典。
什么是动态绑定,我们看下面的栗子:

In [8]: p1.x
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-1ae151c59856> in <module>()
----> 1 p1.x

AttributeError: 'Player' object has no attribute 'x'

In [9]: p1.x = 123

In [10]: p1.__dict__
Out[10]: {'level': 1, 'name': 'Jim', 'stat': 0, 'uid': '0001', 'x': 123}

In [11]: p1.__dict__['y'] = 99

In [12]: p1.y
Out[12]: 99

In [13]: del p1.__dict__['x']

In [14]: p1.x
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-14-1ae151c59856> in <module>()
----> 1 p1.x

AttributeError: 'Player' object has no attribute 'x'

这种动态绑定是以牺牲内存为代价的,因为dict是占用内存的。

In [15]: import sys

In [16]: sys.getsizeof(p1.__dict__)
Out[16]: 864

我们看到这个字典占用了864个字节

为了节省内存,我们关闭动态绑定。

In [17]: p2.x = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-17-ae34f36f4aab> in <module>()
----> 1 p2.x = 1

AttributeError: 'Player2' object has no attribute 'x'

我们无法对p2进行动态绑定,因为p2的属性有哪些已经事先声明过:
slots = ['uid','name','stat','level']

问题三:如何让对象支持上下文管理

问题内容:
实现一个telnet客户端的类TelnetClient,调用实例的start()方法启动客户端与服务器交互,交互完毕后需要调用cleanup()方法,关闭已连接的socket,以及将操作历史记录写入文件并关闭。
能否让TelnetClient的实例支持上下文管理协议,从而替代手动调用cleanup()方法。

解决方案:
实现上下文管理协议,需定义实例的enterexit方法,他们分别在with开始和结束时被调用。

问题四:如何创建可管理的对象属性?

问题内容:在面向对象的编程中,我们把方法(函数)看作对象的接口。直接访问对象的属性可能是不安全的,或设计上不够灵活。但是使用调用方法在形式上不如访问属性简洁。
circle.getRadius()
circle.setRadius(5.0) #繁

circle.radius
circle.radius = 5.0

能否在形式上是属性访问,但实际上调用方法?

解决方案:
使用property函数为类创建可管理属性,fget/fset/fdel对应相应属性访问。

In [1]: from math import  pi

In [6]: class Circle(object):
   ...:     def __init__(self,radius):
   ...:         self.radius = radius
   ...:     def getRadius(self):
   ...:         return self.radius
   ...:     def setRadius(self,value):
   ...:         if not isinstance(value,(int,float)):
   ...:             raise ValueError("wrong type.")
   ...:         self.radius = float(value)
   ...:     def getArea(self):
   ...:         return self.radius **2 *pi
   ...:     R = property(getRadius,setRadius)
   ...:

In [7]: c = Circle(3.2)

In [8]: c.R
Out[8]: 3.2

In [9]: c.R = 5.9

In [10]: c.R
Out[10]: 5.9

使用属性设置对象的特性。

问题五:如何让类支持比较操作?

有时我们希望我们自定义的类之间可以使用比较符进行操作比较,我们自定义比较行为。例如,有一个矩形类,我们希望比较两个矩形的实例,比较的是他们的面积。
class Rectangle:
def init(self,w,h):
self.w = w
self.h = h
def area(self):
return self.w*self.h

rect1 = Rectangle(5,3)
rect2 = Rectangle(4,4)
rect1 > rect2 #=> rect1.area() > rect2.area()

解决方案:
比较符号运算符重载,需要实现以下方法:
ltlegtgeeqne
使用标准库下的functools下的类装饰器total_ordering可以简化此过程。

在我们没有实现 lt函数之前:

In [2]: class Rectangle :
   ...:     def __init__(self,w,h):
   ...:         self.w = w
   ...:         self.h = h
   ...:     def area(self):
   ...:         return self.w*self.h
   ...:

In [3]: rect1 = Rectangle(5,3)

In [4]: rect2 = Rectangle(4,4)

In [5]: rect1 < rect2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-f109d7c52d56> in <module>()
----> 1 rect1 < rect2

TypeError: unorderable types: Rectangle() < Rectangle()

在我们实现之后:

In [6]: class Rectangle :
   ...:     def __init__(self,w,h):
   ...:         self.w = w
   ...:         self.h = h
   ...:     def area(self):
   ...:         return self.w*self.h
   ...:     def __lt__(self,obj):
   ...:         return self.area() < obj.area()
   ...:

In [7]: rect1 = Rectangle(5,3)

In [8]: rect2 = Rectangle(4,4)

In [9]: rect1 < rect2
Out[9]: True

rect1 < rect2 相当于rect1.lt(rect2)
按照同样的方法我们可以实现legtgeeqne方法,不过,我们可以使用装饰器@total_ordering来简化。

In [17]: from functools import  total_ordering

In [18]: @total_ordering
    ...: class Rectangle :
    ...:     def __init__(self,w,h):
    ...:         self.w = w
    ...:         self.h = h
    ...:     def area(self):
    ...:         return self.w*self.h
    ...:     def __lt__(self,obj):
    ...:         return self.area() < obj.area()
    ...:     def __eq__(self,obj):
    ...:         return self.area() == obj.area()
    ...:

In [19]: rect1 = Rectangle(5,3)

In [20]: rect2 = Rectangle(4,4)

In [21]: rect1 <= rect2
Out[21]: True

In [22]: rect1 > rect2
Out[22]: False

我们发现这种情况下只需要里面定义包含等于的两个函数就可以实现全部的。

rect1.lt(rect2) 两边不一定传入同一个类:

下面我们创建一个新类:

In [24]: class Circle(object):
    ...:     def __init__(self,r):
    ...:         self.r = r
    ...:     def area(self):
    ...:         return self.r ** 2 * 3.14
    ...:

In [25]: c1 = Circle(3)

In [26]: rect1 <= c1
Out[26]: True

In [27]: rect1 > c1
Out[27]: False

我们传入的是 c1 当作函数的参数,在类Circle上未加装饰器。因此如果我们将参数对调的情况下,将不成立。

In [28]: c1 >= rect1
Out[28]: True

In [29]: c1 <= rect1
Out[29]: False

In [30]: c1 >= rect2
Out[30]: True

In [31]: c1
Out[31]: <__main__.Circle at 0x288cd5f5ba8>

这一部分黑人问号???

这种方式我们需要在所有的类中都要定义运算符方法重载。

我们为这些图形定义公共的基类shape,并且在shape中实现晕算法重载函数。再额外定义抽象接口area(),子类都要实现area()。

In [32]: from functools import  total_ordering

In [33]: from abc import ABCMeta,abstractmethod

In [34]: @total_ordering
    ...: class Shape(object):
    ...:     @abstractmethod
    ...:     def area(self):
    ...:         pass
    ...:     def __lt__(self,obj):
                 if not isinstance(obj,Shape):
                     raise TypeError('obj is not Shape')
    ...:         return self.area() < obj.area()
    ...:     def __eq__(self,obj):
                 if not isinstance(obj,Shape):
                     raise TypeError('obj is not Shape')
    ...:         return self.area() == obj.area()
    ...:

In [35]: class Rectangle(Shape):
    ...:     def __init__(self,w,h):
    ...:         self.w = w
    ...:         self.h = h
    ...:     def area(self):
    ...:         return self.w*self.h
    ...:

In [36]: class Circle(Shape):
    ...:     def __init__(self,r):
    ...:         self.r = r
    ...:     def area(self):
    ...:         return self.r ** 2 * 3.14
    ...:

In [37]: rect1 = Rectangle(5,3)

In [38]: rect2 = Rectangle(4,4)

In [39]: c1 = Circle(3)

In [40]: rect1 >= c1
Out[40]: False

In [41]: c1 >= rect2
Out[41]: True

问题六:如何使用描述符对实例属性做类型检查?

问题内容:
在项目中,我们实现了一些类,并希望能像静态类型语言那样(C,C++,Java)对它们的实例属性做类型检查。
p = Person()
p.name = 'Bob' #必须是str
p.age = 18 #必须是int
p.height= 1.83 #必须是float

要求:
1,可以对实例变量指定类型
2,赋予不正确类型时抛出异常

解决方案:
使用描述符(就是包含下面三个方法的类,只要包含一个就是描述符)来实现需要类型检查的属性:
分别实现getsetdelete方法,在set内使用isinstance函数做类型检查。

我们新建一个描述符:

In [1]: class Descriptor(object):
   ...:     def __get__(self,instance,cls):
   ...:         print ('in __get__',instance,cls)
   ...:         # return instance.__dict__[xxx]
   ...:     def __set__(self,instance,value):
   ...:         print ('in __set__')
   ...:         # instance.__dict__[xxx] = value
   ...:     def __delete__(self,instance):
   ...:         print ('in __del__')
   ...:         # del instance.__dict__[xxx]
   ...:

我们在另一个类中定义一个类属性,类属性是描述符的实例。

In [2]: class A(object):
   ...:     x = Descriptor()
   ...:

我们创建一个A类的实例,当实例对属性 x 进行操作的时候,会被描述符中对应的函数截获。

In [3]: a = A()

In [4]: a.x
in __get__ <__main__.A object at 0x000002909C8D1748> <class '__main__.A'>

当我们用类访问属性的时候,同样会被函数截获。只不过类的实例值为None。

In [5]: A.x
in __get__ None <class '__main__.A'>

当我们进行赋值操作的时候,会被set函数截获。

In [6]: a.x = 5
in __set__

In [7]: del a.x
in __del__

虽然可以通过a.x访问x。但是x不是实例a的属性。
黑人问号???

下面是一个完整的栗子:

In [10]: class Attr(object):
    ...:     def __init__(self,name,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('excepted an %s' % self.type_)
    ...:         instance.__dict__[self.name] = value
    ...:     def __delete__(self,instance):
    ...:         del instance.__dict__[self.name]
    ...:

In [11]: class Person(object):
    ...:     name = Attr('name',str)
    ...:     age  = Attr('age',int)
    ...:     height = Attr('height',float)
    ...:

In [12]: p = Person()

In [13]: p.name = 'Bob'

In [14]: p.name
Out[14]: 'Bob'

当类型错误的时候会报错
In [15]: p.age = '17'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-d184624312f7> in <module>()
----> 1 p.age = '17'

<ipython-input-10-bf91df0770e3> in __set__(self, instance, value)
      8     def __set__(self,instance,value):
      9         if not isinstance(value,self.type_):
---> 10             raise TypeError('excepted an %s' % self.type_)
     11         instance.__dict__[self.name] = value
     12     def __delete__(self,instance):

TypeError: excepted an <class 'int'>

问题七:如何在环状数据结构中管理内存?

问题内容:
在python中,垃圾回收器通过引用计数来回收垃圾对象(引用计数为0则回收),但是某些环状数据结构(树,图...),存在对象间的循环引用,比如父节点引用子节点,子节点也同时引用父节点。此时同事del 掉引用父子节点,两个对象不能被立即回收。
如何解决此类内存管理问题?

解决方案:
使用标准库weakref,它可以创建一种能访问对象但不增加引用计数的对象。

下面我们先看下引用计数的问题:

In [1]: class A(object):
   ...:     def __del__(self):
   ...:         print ('in __del__')
   ...:
为了看到实例的回收过程,我们定义了析构函数,当实例回收的时候,函数被调用

In [2]: a = A()

In [3]: import sys

使用函数getrefcount()获得引用计数
In [4]: sys.getrefcount(a)
Out[4]: 2

In [5]: sys.getrefcount?
Docstring:
getrefcount(object) -> integer

Return the reference count of object.  The count returned is generally
one higher than you might expect, because it includes the (temporary)
reference as an argument to getrefcount().
Type:      builtin_function_or_method
方法之所以比期望的多一,因为函数本身的参数也有一个引用。

In [6]: sys.getrefcount(a) - 1
Out[6]: 1

我们将对象a复制给对象a2
In [7]: a2 = a

引用计数加1
In [8]: sys.getrefcount(a) - 1
Out[8]: 2

删除对象a2
In [9]: del a2

引用计数
In [10]: sys.getrefcount(a) - 1
Out[10]: 1

当我们将变量a指向对象5,此时A()的对象引用计数为0,调用析构函数,释放对象
In [11]: a = 5
in __del__

下面我们看一个栗子:
我们创建了两个类,当我们创建一个Node的时候,将会添加一个属性data,之后实例化一个Data。data为了知道属于哪个Node,要维护一个Node的引用owner。形成了循环引用。黑人问号???

In [2]: class Data(object):
   ...:     def __init__(self,value,owner):
   ...:         self.owner = owner
   ...:         self.value = value
   ...:     def __str__(self):
   ...:         return "%s's data,value is %s" % (self.owner,self.value)
   ...:     def __del__(self):
   ...:         print ('in Data.__del__')
   ...:

In [3]: class Node(object):
   ...:     def __init__(self,value):
   ...:         self.data = Data(value,self)
   ...:     def __del__(self):
   ...:         print('in Node.__del__')
   ...:

In [4]: node = Node(100)

In [5]: del node

我们发现,当我们删除node的时候,析构函数并没有被调用。也就是对象没有被立即回收掉,可能之后某个时间点会被回收。

我们先看下弱引用:

In [7]: class A(object):
   ...:     def __del__(self):
   ...:         print ('in __del__')
   ...:

In [8]: a =A()

In [9]: import sys

In [10]: sys.getrefcount(a) - 1
Out[10]: 1

In [11]: import weakref

通过weakref生成对象的弱引用
In [12]: a_wref = weakref.ref(a)

我们像函数一样调用弱引用
In [13]: a2 = a_wref()

In [14]: a is a2
Out[14]: True

In [15]: sys.getrefcount(a) - 1
Out[15]: 2

In [16]: del a

In [17]: del a2
in __del__

In [18]: a_wref()

In [19]: a_wref() is None
Out[19]: True

我们看到,当对象存在的时候弱引用返回对象的引用,当对象不存在的时候,弱引用返回None。

我们修改之前的代码,将数据的owner改为对node的弱引用。这样就不会增加node的引用计数了,消除了循环引用的问题,当我们删除node之后,对象将会被回收掉。

In [20]: class Data(object):
    ...:     def __init__(self,value,owner):
    ...:         self.owner = weakref.ref(owner)
    ...:         self.value = value
    ...:     def __str__(self):
    ...:         return "%s's data,value is %s" % (self.owner(),self.value)
    ...:     def __del__(self):
    ...:         print ('in Data.__del__')
    ...:
以函数调用的方式使用弱引用
In [21]: class Node(object):
    ...:     def __init__(self,value):
    ...:         self.data = Data(value,self)
    ...:     def __del__(self):
    ...:         print('in Node.__del__')
    ...:

In [22]: node = Node(100)

In [23]: del node
in Node.__del__
in Data.__del__

问题八:如何通过实例方法名字的字符串调用方法?

推荐阅读更多精彩内容