Python 黑魔法之内存优化 & 属性拦截(__slots__,_setattr__,__getattr__,__delattr__)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013486414/article/details/85112411

写在之前

围绕类的话题,说是说不完的,仅在特殊方法,除了我们在前面遇到过的 init(),new(),str() 等之外还有很多。虽然它们只是在某些特殊的场景中才会用到,但是学会它们却可以成为你熟悉这门语言路上的铺路石。

所以我会在试图介绍一些「黑魔法」,让大家多多感受一下 Python 的魅力所在,俗话说「艺多不压身」就是这个道理了。

内存优化

首先先让我们从复习前面的类属性和实例属性的知识来引出另一个特殊方法:

>>> class Sample:
...    name = 'rocky'
...

就像前面的文章我们所说的,每个类都有一个 dict() 属性,它包含了当前类的类属性:

>>> Sample.__dict__
mappingproxy({'__module__': '__main__', 'name': 'rocky', '__dict__': <attribute '__dict__' of 'Sample' objects>, '__weakref__': <attribute '__weakref__' of 'Sample' objects>, '__doc__': None})
>>> Sample.name
'rocky'

同样,如果我们创建了实例,每个实例也有一个 dict 属性,它里面就是当前的实例属性:

>>> a = Sample()
>>> a.__dict__
{}
>>> a.age = 23
>>> a.__dict__
{'age': 23}

上面的操作可以看出,当实例刚刚创建的时候,dict 是空的,只有创建了实例属性以后,它才包含其内容。实例的 dict 和类的 dict 是有所区别的,即实例属性和类属性是不同的。

从理论上来说,我们可以根据一个类创建无数的实例,新建一个实例以后,又创建了一个新的 dict,这将是一个很可怕的事情,虽然每个 dict 所占的内存空间很小,当然这件事事实上是不会出现的。但是程序不能建立在这种不可靠的猜测的基础上,程序要对过程有明确的控制。

所以就要有一种方法能够控制 dict,于是「slots」应运而生。

>>> class Nature:
...    __slots__ = ('tree','flower')
... 
>>> dir(Nature)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'flower', 'tree']

我们仔细来看 dir() 的结果,发现 dict 属性没有了,也就是说 slotsdict 挤出去了,它进入了类的属性。

>>> Nature.__slots__
('tree', 'flower')

从这里可以看出,类 Nature 有且仅有两个属性。从类的角度来看,其类属性只有这两个;从实例的角度来看,其实例属性也只有这两个。

>>> Nature.tree = 'liushu'
>>> Nature.tree
'liushu'
>>> Nature.tree = 'lishu'
>>> Nature.tree
'lishu'

通过类可以对属性进行赋值和修改,这个似乎和以前的类属性没有什么区别,别着急,继续往下看就看到区别了:

>>> x = Nature()
>>> x.__slots__
('tree', 'flower')
>>> y = Nature()
>>> y.__slots__
('tree', 'flower')
>>> id(x.__slots__)
4531629384
>>> id(y.__slots__)
4531629384

你看,实例化以后,实例的 slots 和类的 slots 完全一样,这跟前面的 dict 大不一样了。并且我们建立了两个实例,结果发现两个实例的 slots 在内存中居然是一个,或者可以说是增加实例时 slots 并不增加。

>>> x.tree
'lishu'
>>> y.tree
'lishu'

既然类属性已经赋值,那么通过任何一个实例属性都能得到同样的值,不过这时候不能通过实例修改此属性的值。

>>> x.tree = 'taoshu'
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: 'Nature' object attribute 'tree' is read-only

对实例属性来说,类的静态数据是只读的,不能修改,只有通过类属性才能修改。但对于尚未赋值的属性,能够通过实例赋值。

>>> x.flower = 'rose'
>>> x.flower
'rose'
>>> x.flower = 'moli'

显然通过实例操作的属性,也能够通过实例修改,但是实例属性的值并不能够修改类属性的值

Nature.flower
<member 'flower' of 'Nature' objects>

由上面可以看出,实例属性的值并没有传回给类属性,也可以理解为新建了一个同名字的实例属性,如果再给类属性赋值的话,则会像下面一样:

>>> Nature.flower = 'huaihua'
>>> x.flower
'huaihua'

类属性对实例属性具有决定作用,对实例而言,通过类所定义的属性都是只读的。

slots 已经把实例属性牢牢的看管起来,只能是指定的属性,如果想要增加属性的话,只能通过类属性来实现,所以 slots 的一个重要作用就是优化了内存。

我们访问某个类或者是实例属性的时候,如果它不存在的话,就会出现异常。对于异常,我们总是要处理的,接下来就让我们来看,是怎么处理的。

属性拦截

在 Python 中,就有一些具有拦截功能的方法,让我们先来看一下:

1.setattr(self,name,value):如果想要给 name 赋值的话,就需要调用这个方法。

2.getattr(self,name):如果 name 被访问且它又不存在,那么这个方法将被调用。

3.delattr(self,name):如果要删除 name 的话,这个方法就要被调用了。

下面我们用例子来演示一下:

>>> class Sample:
...    def __getattr__(self,name):
...            print('hello getattr')
...    def __setattr__(self,name,value):
...            print('hello setattr')
...            self.__dict__[name] = value
...

上面的例子中类 Sample 只有两个方法,下面让我们实例化一下:

>>> s = Sample()
>>> s.x
hello getattr

s.x 这个实例属性本来是不存在的,但是由于类中有了 getattr(self,name) 方法,当发现属性 x 不存在于对象的 dict 中时,就调用了 getattr,也就是所谓的「拦截成员」。

>>> s.x = 7
hello setattr

同理,给对象的属性赋值的时候,调用了 setattr(self,name,value) 方法,这个方法中有 self.dict[name] = value,通过这个就将属性和数据存到了对象 dict 中。如果再调用这个属性的话,会成为下面这样:

>>> s.x
7

出现这种结果的原因是它已经存在于对象的 dict 中了。

看了上面的两个,你是不是觉得上面的方法很有魔力呢?这就是「黑魔法」,但是它具体有什么应用呢?我们接着来看:

class Rectangle:
   """
   the width and length of Rectangle
   """

   def __init__(self):
       self.width = 0
       self.length = 0

   def setSize(self,size):
       self.width, self.length = size

   def getSize(self):
       return self.width, self.length

if __name__ == "__main__":
   r = Rectangle()
   r.width = 3
   r.length = 4
   print(r.getSize())
   print(r.setSize((30,40)))
   print(r.width)
   print(r.length)

上面是我根据一个很有名的例子改编的,你可以先想一下结果,想完以后可以接着往下看:

(3, 4)
30
40

这段代码运行的结果如上面所示,作为一个强迫证的码农,对于这种可以改进的程序当然不能容忍。我们在上面介绍的特殊方法,我们一定要学以致用,虽然重新写的不一定比原来的好,但我们还是要尝试去用:

class NewRectangle:
   """
   the width and length of Rectangle
   """

   def __init__(self):
       self.width = 0
       self.length = 0

   def __setattr__(self, name, value):
       if name == 'size':
           self.width, self.length = value
       else:
           self.__dict__[name] = value

   def __getattr__(self, name):
       if name == 'size':
           return self.width, self.length
       else:
           return AttributeError

if __name__ == "__main__":
   r = NewRectangle()
   r.width = 3
   r.length = 4
   print(r.size)
   r.size = 30,40
   print(r.width)
   print(r.length)

我们来看一下运行的结果:

(3, 4)
30
40

我们可以看到,除了类的写法变了以外,调用的方式没有变,结果也没有变。

写在之后

黑魔法我暂时先说这两个,其实还有别的,如果你感兴趣的话,可以去 Google 一些文章来看,相信自己的探索会让自己收获更多。

至此,不知道你发现了没有,我们使用了很多以双下划线开头和结尾的属性和方法,比如 init 等。在 Python 中,以双下划线开头和结尾的都是「特殊」的,当然这个只是一个惯例,目的是为了确保这些特殊的名字不会和你定义的名字发生冲突。如果你看着它们不顺眼,想要把它们重写当然也是可以的。

更多内容,欢迎关注公众号「Python空间」,期待和你的交流。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/u013486414/article/details/85112411