PERSONAL BLOG
Category: Programming

Slots in Python

Feb. 11, 2024

Python is a dynamic programming language. Here you're allowed to do whatever you wish with objects and their instances - you can add attributes, remove them etc.

E.g.

class Teapot:
    pass

>>> obj = Teapot()
>>> obj.sugar_cubes_count = 15
>>> obj.sugar_cubes_count
# 15
>>> obj.leaves = ["Mint", "Chamomile"]
>>> obj.leaves
["Mint", "Chamomile"]

Of course, in most cases you don't want to do this. Anyway, this ability is really convenient and gives a lot of flexibility... But.. We're going to pay for this. Pay with a deacresed attributes speed access and increased memory consumption.

But what if we don't need so flexible approach? What if we're ok to declare every attribute manually - can we avoid the price then? Yes, we can.

This is where magic attribute __slot__ comes in. He allows to limit  a set of available attributes for an object

class SugarCube:
    __slots__ = ('width', 'height')

>>> obj = SugarCube()
>>> obj.width = 5
>>> obj.width
# 5
>>> obj.color = 'Brown'
Traceback (most recent call last):
  File "python", line 5, in <module>
AttributeError: 'SugarCube' object has no attribute 'color'

So, since now we're not allowed to add custom attributes to instances. Will it increase the performance?

class Foo: __slots__ = ('foo',)
class Bar: pass


def get_set_delete(obj):
    obj.foo = 'foo'
    obj.foo
    del obj.foo

def test_foo():
    get_set_delete(Foo()) 

def test_bar():
    get_set_delete(Bar())

and let's estimate the execution time using timeit module

>>> import timeit 
>>> min(timeit.repeat(test_foo))
0.18728888700024982
>>> min(timeit.repeat(test_bar))
0.23361162799983504

So, the class with __slots__ is 20% faster on the attribute accessioning. The numbers, of course, can be different for different versions of interpretator, OS etc.

What about memory? Every object instance has magic attribute __dict__. This is a dictionary of all available attributes of the instance. Every instance has it.

class BigLebowski:
    pass

>>> obj = BigLebowski()
>>> obj.__dict__
# {}
>>> obj.milk = True
>>> obj.__dict__
# {'milk': True} 

When the __slots__ attribute is declared explicitly, the __dict__dictionary won't exist:

class LasVegas:
    __slots__ = ('fear', 'loathing')

>>> obj = SlotsClass()
>>> obj.fear = 5
>>> obj.__slots__
# ('fear', 'loathing')
>>> obj.__dict__
Traceback (most recent call last):
  File "python", line 8, in <module>
AttributeError: 'LasVegas' object has no attribute '__dict__'

This is how the memory is saved

Attributes count Using __slots__ (bytes) No using __slots__ (bytes)
0 32 152
1 64 232
2 104 320
6 264 712
11 464 1240
22 904 2376
43 1744 4568

 

As you can see, __slots__ are easy... BUT.... there are some things you should consider:

  1. Inheritance
    __slots__ value inherits, but doesn't prevent of __dict__ creation. So, child classes will allow to add dynamic attributes.
    class Fruit:
        __slots__ = ('color', 'shape')
    
    class Cucumber(Fruit):
        pass
    
    >>> obj = Cucumber()
    >>> obj.__slots__
    # ('color', 'shape')
    >>> obj.color = 'Green'
    >>> obj.is_vegatable = False
    >>> obj.__dict__
    # {'is_vegatable': False}
    
    If you want to limit subclasses by slots, you will need to declare __slots__ there. BTW, you don't need to duplicate slots declared into the base class
    class Fruit:
        __slots__ = ('color', 'shape')
    
    class Cucumber(Fruit):
        __slots__ = ('is_vegetable', )
    
    >>> obj = Cucumber()
    >>> obj.color = 'Green'
    >>> obj.is_vegetable = False
    >>> obj.something_new = 3
    Traceback (most recent call last):
      File "python", line 12, in <module>
    AttributeError: 'Cucumber' object has no attribute 'something_new'
  2. It's not going to work with multiple inheritance. If there are two base classes with declared __slots__ attribute, an attempt to create a child class will fail.
    class BaseA(object): 
        __slots__ = ('a',)
    
    class BaseB(object): 
        __slots__ = ('b',)
    
    >>> class Child(BaseA, BaseB): __slots__ = ()
    Traceback (most recent call last):
      File "<pyshell#68>", line 1, in <module>
        class Child(BaseA, BaseB): __slots__ = ()
    TypeError: Error when calling the metaclass bases multiple bases have instance lay-out conflict
  3. The last problem - classes with defined __slots__ attribute doesn't support weakrefs. It's caused by the fact, that declaring __slots__ prevents of creation __dict__ method as well as of __weakref__ method. The last one provides weakref support. The solution is simple - you just need to add __weakref__ into the __slots__ list