Usage of __slots__?

Viewed 320070

What is the purpose of __slots__ in Python — especially with respect to when I would want to use it, and when not?

14 Answers

Quoting Jacob Hallen:

The proper use of __slots__ is to save space in objects. Instead of having a dynamic dict that allows adding attributes to objects at anytime, there is a static structure which does not allow additions after creation. [This use of __slots__ eliminates the overhead of one dict for every object.] While this is sometimes a useful optimization, it would be completely unnecessary if the Python interpreter was dynamic enough so that it would only require the dict when there actually were additions to the object.

Unfortunately there is a side effect to slots. They change the behavior of the objects that have slots in a way that can be abused by control freaks and static typing weenies. This is bad, because the control freaks should be abusing the metaclasses and the static typing weenies should be abusing decorators, since in Python, there should be only one obvious way of doing something.

Making CPython smart enough to handle saving space without __slots__ is a major undertaking, which is probably why it is not on the list of changes for P3k (yet).

You would want to use __slots__ if you are going to instantiate a lot (hundreds, thousands) of objects of the same class. __slots__ only exists as a memory optimization tool.

It's highly discouraged to use __slots__ for constraining attribute creation.

Pickling objects with __slots__ won't work with the default (oldest) pickle protocol; it's necessary to specify a later version.

Some other introspection features of python may also be adversely affected.

Each python object has a __dict__ atttribute which is a dictionary containing all other attributes. e.g. when you type self.attr python is actually doing self.__dict__['attr']. As you can imagine using a dictionary to store attribute takes some extra space & time for accessing it.

However, when you use __slots__, any object created for that class won't have a __dict__ attribute. Instead, all attribute access is done directly via pointers.

So if want a C style structure rather than a full fledged class you can use __slots__ for compacting size of the objects & reducing attribute access time. A good example is a Point class containing attributes x & y. If you are going to have a lot of points, you can try using __slots__ in order to conserve some memory.

In addition to the other answers, __slots__ also adds a little typographical security by limiting attributes to a predefined list. This has long been a problem with JavaScript which also allows you to add new attributes to an existing object, whether you meant to or not.

Here is a normal unslotted object which does nothing, but allows you to add attributes:

class Unslotted:
    pass
test = Unslotted()
test.name = 'Fred'
test.Name = 'Wilma'

Since Python is case sensitive, the two attributes, spelled the same but with different case, are different. If you suspect that one of those is a typing error, then bad luck.

Using slots, you can limit this:

class Slotted:
    __slots__ = ('name')
    pass
test = Slotted()
test.name = 'Fred'      #   OK
test.Name = 'Wilma'     #   Error

This time, the second attribute (Name) is disallowed because it’s not in the __slots__ collection.

I would suggest that it’s probably better to use __slots__ where possible to keep more control over the object.

Beginning in Python 3.9, a dict may be used to add descriptions to attributes via __slots__. None may be used for attributes without descriptions, and private variables will not appear even if a description is given.

class Person:

    __slots__ = {
        "birthday":
            "A datetime.date object representing the person's birthday.",
        "name":
            "The first and last name.",
        "public_variable":
            None,
        "_private_variable":
            "Description",
    }


help(Person)
"""
Help on class Person in module __main__:

class Person(builtins.object)
 |  Data descriptors defined here:
 |
 |  birthday
 |      A datetime.date object representing the person's birthday.
 |
 |  name
 |      The first and last name.
 |
 |  public_variable
"""

You have — essentially — no use for __slots__.

For the time when you think you might need __slots__, you actually want to use Lightweight or Flyweight design patterns. These are cases when you no longer want to use purely Python objects. Instead, you want a Python object-like wrapper around an array, struct, or numpy array.

class Flyweight(object):

    def get(self, theData, index):
        return theData[index]

    def set(self, theData, index, value):
        theData[index]= value

The class-like wrapper has no attributes — it just provides methods that act on the underlying data. The methods can be reduced to class methods. Indeed, it could be reduced to just functions operating on the underlying array of data.

In addition to the myriad advantages described in other answers herein – compact instances for the memory-conscious, less error-prone than the more mutable __dict__-bearing instances, et cetera – I find that using __slots__ offers more legible class declarations, as the instance variables of the class are explicitly out in the open.

To contend with inheritance issues with __slots__ declarations I use this metaclass:

import abc

class Slotted(abc.ABCMeta):
    
    """ A metaclass that ensures its classes, and all subclasses,
        will be slotted types.
    """
    
    def __new__(metacls, name, bases, attributes, **kwargs):
        """ Override for `abc.ABCMeta.__new__(…)` setting up a
            derived slotted class.
        """
        if '__slots__' not in attributes:
            attributes['__slots__'] = tuple()
        
        return super(Slotted, metacls).__new__(metacls, name, # type: ignore
                                                        bases,
                                                        attributes,
                                                      **kwargs)

… which, if declared as the metaclass of the base class in an inheritance tower, ensures that everything that derives from that base class will properly inherit __slots__ attributes, even if an intermediate class fails to declare any. Like so:

# note no __slots__ declaration necessary with the metaclass:
class Base(metaclass=Slotted):
    pass

# class is properly slotted, no __dict__:
class Derived(Base):
    __slots__ = 'slot', 'another_slot'

# class is also properly slotted:
class FurtherDerived(Derived):
    pass
Related