1. slots的用法
1.1 基本用法
之前学习python的时候,知道使用slots能够节省内存,然而却没有在实际项目中使用过,而且也不清楚为什么能够节省内存?能够节省多少内存?记忆总是那么脆弱,那么干脆来个彻底的探索,并记录之。 首先,我们看看slots的基础用法:
class A(object):
__slots__ = ['name', 'attr']
def __init__(self, name, attr):
self.name = name
self.attr = attr
在这里,我们定义了一个类A,它有两个属性name和attr,这样,在A的实例中我们就可以使用name和attr这两个属性了,但是使用一个没有包含在__slots__中的属性就会出错,例如:
那么,到底能省多少内存呢?我们用实例来测量下。
1.2 内存测量工具
要测量内存使用,就需要工具。ipython_memeory_usage是IPython下的实时监测每一行命令所使用的内存的工具,它可以帮助你了解每一个命令用的内存以及花费的时间。(github的readme说它不支持python 2.7,但经我实测一些简单功能还是支持的,更多的没有测试过,为保险起见,可以装python 3版本的,但是如果没有安装python 3,只是简单使用装Python 2版本应该也可以,暂时没有发现问题,如果电脑同时装有python2和3,在Windows下,可以使用 py -2命令和py -3命令分别进入python2和python3,同理,用pip install 和pip3 install分别安装python2和3的第三方库)。
在安装ipython_memory_usage之前,需要首先安装Memory Profiler。直接通过pip install安装会出错,需要到github上(https://github.com/pythonprofilers/memory_profiler)下载安装包,然后通过python setup.py install进行安装。它是一个监测进程中内存消耗的模块,使用方法如下:
from memory_profiler import profile
@profile
def my_func():
a = [1] * (10 ** 6)
b = [2] * (2 * 10 ** 7)
del b
return a
假设将其保存为example.py,则运行python example.py,可以得到类似下面的结果:
Line # Mem usage Increment Line Contents
==============================================
3 @profile
4 5.97 MB 0.00 MB def my_func():
5 13.61 MB 7.64 MB a = [1] * (10 ** 6)
6 166.20 MB 152.59 MB b = [2] * (2 * 10 ** 7)
7 13.61 MB -152.59 MB del b
8 13.61 MB 0.00 MB return a
每一行的Mem Usage表示当前一共用的内存,Increment标明了这一行使用的内存大小。
安装完Memeory Profiler,我们便可以到ipython_memory_usage的github(https://github.com/ianozsvald/ipython_memory_usage)上下载安装包并进行安装,安装完之后,便可以简单的使用它进行python命令的内存与时间的测量,如下图所示:
从图中我们可以看到,import numpy这个库消耗了14.4766M内存,而生成一个10000000维的数组消耗了76.6484M内存。
1.3 _slots__内存使用和存取速度实测
1.3.1 内存使用测试
最后,我们终于可以进行我们的slots内存测试实验了,如下图:
从实验结果可以看到,对于有两个属性的实例,生成1M个实例,使用slots消耗了49.6250M内存,而没有使用slots消耗了185.9219M内存,使用slots的内存消耗大概为不使用slots的26.69%,内存节省量还是很可观的。
1.3.2 存取速度测试
使用__slots__不但能够节省内存,也能提高存取属性的效率,下面用一个例子验证:
在本人电脑上(python 2.7+windows),测试速度有10%左右的提升。
2. __slots__使用注意事项
虽然使用slots能够节省内存和存取属性的时间,但是也有很多要注意的地方。
2.1 要使用slots,类必须继承自object,而且为了不生成dict,继承体系中的类必须定义slots
如下图所示,我们定义了三个类A、B、C,B和C继承自A,A定义了slots属性attr,B定义了一个空的slots,而C没有定义slots,ob和oc分别是B和C的实例。
接下来,我们分别我ob和oc赋予属性attr2,可以看到,为ob赋予属性时提示没有属性attr2,而为oc赋予属性成功了:
我们查看ob和oc的全部属性,发现oc中生成了dict:
查看oc的dict:
In [14]: oc.__dict__
Out [14]: {'attr2': 'cc'}
所以即使父类定义了slots,子类要不生成dict,也要定义空的slots(如果不想引入新的属性的话)。 那么,如果没有继承自object会怎么样呢?我们定义一个具有slots的旧类D,然后对它进行实例化:
可以发现,__slots__并没有起限制作用.
2.2 使用slots的多继承问题
由于__slots__的实现不是简单的列表或者字典,多个父类的非空__slots__不能直接合并,所以使用时会报错(即使多个父类的非空__slots__是相同的)。
如果要使用多继承,那就要使用多个空__slots__的父类,这是关于使用__slots__多继承的唯一办法。
2.3 不要只在生成大量对象的实例时使用__slots__
collections模块的抽象基类虽然不能实例化,但是它们也声明了__slots__,为什么呢?因为如果用户希望继承自collections中的基类的类不要创建__dict__和__weakref,那么父类也必须没有这些属性,所以抽象基类要定义__slots__,以避免不必要的空间使用。
3. 一探究竟——__slots__为什么能够加快属性访问速度和减少内存消耗?
在1.3节我们经过实验验证,使用__slots__能够极大的节省内存,并且存取属性的速率也有所提升,这是为什么呢?《Learning Python》在第38章 Managed Attributes中的Descriptors中有讲到,python的property和slots都是用descriptor实现的,那么我们就从descriptor(描述符)讲起。
3.1 python descriptor(描述符)
在Python中,描述符协议使得我们能够截取特定属性的存取和删除,描述符是一个单独的类,它被赋值给一个类属性,这样就能够截取对这个类属性的获取、赋值和删除了。 一个描述符应该是这样的:
class Descriptor(object):
""docstring goes here"""
def __get__(self, instance, owner):...
def __set__(self, instance, value):...
def _delete__(self, instance):...
一个有其中任意一个方法的类就被当做描述符。这里的self是描述符的实例,instarnce是描述符实例被赋予的那个类的实例,owner是描述符实例被赋予的那个类。
class Name(object):
"""name descriptor docs"""
def __get__(self, instance, owner):
print 'fetch...'
return instance._name
def __set__(self, instance, value):
print 'change...'
instance._name = value
def __delete__(self, instance):
print 'remove...'
del instance._name
class Person(object):
def __init__(self, name):
self._name = name
name = Name()
if __name__ == '__main__':
bob = Person("Bob Smith")
print bob.name
bob.name = 'Robert Smith'
print bob.name
del bob.name
输出是这样的:
C:\Python27\python.exe D:/LearningPython/test.py
fetch...
Bob Smith
change...
fetch...
Robert Smith
remove...
3.2 用纯python实现slots
现在,我们用纯python写一个简单的__slots__的实现。这里用到了元类(metaclass),有关元类,stack overflow上有个很热的帖子What is a metaclass in Python?,或者有人翻译成了中文版深刻理解Python中的元类.
class Member(object):
# 定义描述器实现slots属性的查找
def __init__(self, i):
self.i = i
def __get__(self, instance, owner):
return instance._slotvalues[self.i]
def __set__(self, instance, value):
instance._slotvalues[self.i] = value
class Type(type):
# 使用元类实现slots
def __new__(cls, name, bases, attrs):
slots = attrs.get('_slots_')
if slots:
for i, slot in enumerate(slots):
attrs[slot] = Member(i)
attrs['_slotvalues'] = [None] * len(slots)
return type.__new__(cls, name, bases, attrs)
class Object(object):
__metaclass__ = Type
class A(Object):
_slots_ = 'x', 'y'
if __name__ == '__main__':
a = A()
print dir(A)
a.x = 10
print a.x
在CPython中,当一个A类定义了__slots__=('x', 'y'),A.x就是一个有__get__和__set__方法的member_descriptor,并且在每个实例中可以通过直接访问内存(direct memory access)获得。所以访问A.dict和A.x的速度是相近的。
在上面的例子中,我们用纯python实现了一个等价的slots,当一个元类看到_slots_的定义,就创建相应的描述符属性,并为实例创建一个_slotvalues列表。
这个例子与CPython不同的是: 1. 例子中_slotvalues是一个存储在类对象外部的列表,而在Cpython中它与实例对象存储在一起,可以通过直接访问内存获得。相应地, member_descriptor也不是存在外部列表,而同样可以通过直接访问内存获得。 2. 默认情况下,new方法会为每个实例创建一个字典dict来存储实例的属性,但如果定义了__slots__,__new__方法就不会再创建这个字典,以及__weakref__,但我们的例子没有实现这个功能。
3.3 更快的属性访问
默认情况下,访问一个实例的熟悉是通过访问__dict__来实现的,例如访问a.x就相当于访问a.__dict__['x'],可以简单的理解为四步: 1. a.x 2. a.__dict__ 3. a.__dict__[x] 4. 结果 而定义了__slots__的类会为每个属性创建一个描述器,访问属性时就直接调用这个描述器,可以简单的理解为三步: 1. b.x 2. member descriptor 3. 结果 访问__dict__和访问描述器速度是相近的,所以通过__dict__访问相当于多了一步字典访问(哈希函数)的消耗。所以使用__slots__的类的属性访问速度会稍微快点,1.3.2的实验结果也验证了这点。
3.4 减少内存消耗
虽然使用__slots__能够稍微提高属性访问速度,但是节省内存才是它最大的用途。python 的字典本质上是个哈希表,它是一种以空间换时间的数据结果,而且为了解决冲突,python会在字典使用量超过2/3时,会进行2-4倍的扩容。而slots使用了描述符,只会为属性存储分配必要的内存,所以能够大幅减少内存的使用。