Python Class Attribute __slots__

27 Jul 2017

There is a lesser-known feature of Python classes: class attribute __slots__.

Normally, each instance object x of class C has a dictionary x.__dict__ which let you bind arbitrary attributes on x. When a class C has a class attribute __slots__ defined (a tuple of strings), an instance x of class C has no x.__dict__, and any attempt to bind on x any attribute whose name is not in C.__slots__ raises an exception.

This means __slots__ attribute reduces memory consumption for instance objects. The saving is a only few tens of bytes per instance, but significant when the number of instances grows to millions/tens of thousands.

Let’s do some experiments with iPython. The snippets below assume memory_profiler is installed (pip install memory_profiler).

First let’s profile some code in Python 2.7 (2.7.12) in iPython.

In [1]: %load_ext memory_profiler

In [2]: class Bob(object):
   ...:     __slots__ = ('x', 'y', 'z')
   ...:     def __init__(self):
   ...:         self.x = 'wtf'
   ...:         self.y = 5
   ...:         self.z = (5, 2,)
   ...:         

In [3]: %memit bobs = [Bob() for _ in xrange(1000000)]
peak memory: 99.65 MiB, increment: 77.96 MiB

In [4]: class Alice(object):
   ...:     def __init__(self):
   ...:         self.x = 'wtf'
   ...:         self.y = 5
   ...:         self.z = (5, 2,)
   ...:

In [5]: %memit alices = [Alice() for _ in xrange(1000000)]
peak memory: 448.24 MiB, increment: 348.51 MiB

The result speaks for itself; using __slots__ attributes does save A LOT of memory for large amount of simple objects, at least in Python 2.7. Now let’s see if this still holds for Python 3 (3.6.1).

In [1]: %load_ext memory_profiler

In [2]: class Bob:
   ...:     __slots__ = ('x', 'y', 'z')
   ...:     def __init__(self):
   ...:         self.x = 'wtf'
   ...:         self.y = 5
   ...:         self.z = (5, 2,)
   ...:         

In [3]: %memit bobs = [Bob() for _ in range(1000000)]
peak memory: 106.08 MiB, increment: 69.86 MiB

In [4]: class Alice:
   ...:     def __init__(self):
   ...:         self.x = 'wtf'
   ...:         self.y = 5
   ...:         self.z = (5, 2,)
   ...:         

In [5]: %memit alices = [Alice() for _ in range(1000000)]
peak memory: 277.33 MiB, increment: 170.72 MiB

According to the result from Python 3.6, it looks like __slots__ still saves memory, but not as dramatic as in Python 2; and overall Python 3.6 seems to use much less memory than Python 2.7 for instances without __slots__. Some more discussions on StackOverflow can be found here for deeper understanding.

To conclude, using __slots__ does save memory usage, and the main saving comes from avoiding using __dict__ in instances objects. And, since Python 3.3, such savings is not so impressive anymore due to an improvement in the implementation of attribute dictionaries (PEP0412, and see Python Tutorial on Slots).