预计阅读本页时间:-
10.4 namedtuple和slots
有时能创建只拥有一些固定属性的简单对象是非常有用的。一个简单的实现可能需要下面这几行代码:
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
这肯定可以满足需求。但是,这种方法的缺点就是它创建了一个继承自object的类。在使用这个Point类时,需要实例化对象。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
Python中这类对象的特性之一就是会存储所有的属性在一个字典内,这个字典本身被存在__dict__属性中:
>>> p = Point(1, 2)
>>> p.__dict__
{'y': 2, 'x': 1}
>>> p.z = 42
>>> p.z
42
>>> p.__dict__
{'y': 2, 'x': 1, 'z': 42}
好处是可以给一个对象添加任意多的属性。缺点是通过字典来存储这些属性内存方面的开销很大,要存储对象、键、值索引等。创建慢,操作也慢,并且伴随着高内存开销。看看下面这个简单的类。
[source, python]
class Foobar(object):
def __init__(self, x):
self.x = x
我们通过Python包memory_profiler来检测一下内存使用情况:
$ python -m memory_profiler object.py
Filename: object.py
Line # Mem usage Increment Line Contents
================================================
5 @profile
6 9.879 MB 0.000 MB def main():
7 50.289 MB 0.410 MB f = [ Foobar(42) for i in range(100000) ]
因此,使用对象但不使用这个默认行为的方式是存在的。Python中的类可以定义一个__slots__属性,用来指定该类的实例可用的属性。其作用在于可以将对象属性存储在一个list对象中,从而避免分配整个字典对象。如果浏览一下CPython的源代码并且看看Objects/typeobject.c文件,就很容易理解这里Python做了什么。下面给出了相关处理函数的部分代码:
static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
[...]
/* Check for a __slots__ sequence variable in dict, and count it */
slots = _PyDict_GetItemId(dict, &PyId___slots__);
nslots = 0;
if (slots == NULL) {
if (may_add_dict)
add_dict++;
if (may_add_weak)
add_weak++;
}
else {
/* Have slots */
/* Make it into a tuple */
if (PyUnicode_Check(slots))
slots = PyTuple_Pack(1, slots);
else
slots = PySequence_Tuple(slots);
/* Are slots allowed? */
nslots = PyTuple_GET_SIZE(slots);
if (nslots > 0 && base->tp_itemsize != 0) {
PyErr_Format(PyExc_TypeError,
"nonempty __slots__ "
"not supported for subtype of '%s'",
base->tp_name);
goto error;
}
/* Copy slots into a list, mangle names and sort them.
Sorted names are needed for __class__ assignment.
Convert them back to tuple at the end.a
*/
newslots = PyList_New(nslots - add_dict - add_weak);
if (newslots == NULL)
goto error;
if (PyList_Sort(newslots) == -1) {
Py_DECREF(newslots);
goto error;
}
slots = PyList_AsTuple(newslots);
Py_DECREF(newslots);
if (slots == NULL)
goto error;
}
/* Allocate the type object */
type = (PyTypeObject *)metatype->tp_alloc(metatype, nslots);
[...]
/* Keep name and slots alive in the extended type object */
et = (PyHeapTypeObject *)type;
Py_INCREF(name);
et->ht_name = name;
et->ht_slots = slots;
slots = NULL;
[...]
return (PyObject *)type;
正如你所看到的,Python将__slots__的内容转化为一个元组,构造一个list并排序,然后再转换回元组并存储在类中。这样Python就可以快速地抽取值,而无需分配和使用整个字典。
声明这样一个类并不难,如示例10.8所示。
示例 10.8 使用__slots__的类声明
class Foobar(object):
__slots__ = 'x'
def __init__(self, x):
self.x = x
可以很容易地通过memory_profiler比较两种方法的内存占用情况,如示例10.9所示。
示例 10.9 使用了__slots__的对象的内存占用
% python -m memory_profiler slots.py
Filename: slots.py
Line # Mem usage Increment Line Contents
================================================
7 @profile
8 9.879 MB 0.000 MB def main():
9 21.609 MB 11.730 MB f = [ Foobar(42) for i in range(100000) ]
看似通过使用Python类的__slots__属性可以将内存使用率提升一倍,这意味着在创建大量的简单对象时使用__slots__属性是有效且高效的选择。但这项技术不应该被滥用于静态类型或其他类似场合,那不是Python程序的精神所在。
由于属性列表的固定性,因此不难想象类中列出的属性总是有一个值,且类中的字段总是按某种方式排过序的。
这也正是collection模块中namedtuple类的本质。它允许动态创建一个继承自tuple的类,因而有着共有的特征,如不可改变,条目数固定。namedtuple所提供的能力在于可以通过命名属性获取元组的元素而不是通过索引,如示例10.10所示。
示例 10.10 用namedtuple声明类
>>> import collections
>>> Foobar = collections.namedtuple('Foobar', ['x'])
>>> Foobar = collections.namedtuple('Foobar', ['x', 'y'])
>>> Foobar(42, 43)
Foobar(x=42, y=43)
>>> Foobar(42, 43).x
42
>>> Foobar(42, 43).x = 44
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> Foobar(42, 43).z = 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Foobar' object has no attribute 'z'
>>> list(Foobar(42, 43))
[42, 43]
因为这样的类是继承自tuple的,因此可以很容易将其转换为list。但不能添加或修改这个类的对象的任何属性,因为它继承自tuple同时也因为__slots__的值被设置成了一个空元组以避免创建__dict__。基于collections.namedtuple构建的类的内存占用如示例10.11所示。
示例 10.11 基于collections.namedtuple构建的类的内存占用
% python -m memory_profiler namedtuple.py
Filename: namedtuple.py
Line # Mem usage Increment Line Contents
================================================
4 @profile
5 9.895 MB 0.000 MB def main():
6 23.184 MB 13.289 MB f = [ Foobar(42) for i in range(100000) ]
因此,namedtuple类工厂的使用同使用带有__slots__的对象一样有效,唯一的不同在于它同tuple类兼容。因此,它可以作为参数传入任何期望iterable类型参数的原生Python函数。同时它也享有已有的针对元组的优化。1
namedtuple还提供了一些额外的方法,尽管以下划线作为前缀,但实际上是可以公开访问的。_asdict可以将namedtuple转换为字典实例,_make可以转换已有的iterable对象为namedtuple,_replace替换某些字段后返回一个该对象的新实例。