2.7. Slots

  • Faster attribute access

  • Space savings in memory (overhead of dict for every object)

  • Prevents from adding new attributes

  • The space savings is from:

  • Store value references in slots instead of __dict__

  • Denying __dict__ and __weakref__ creation if parent classes deny them and you declare __slots__

class Astronaut:
    __slots__ = ('firstname', 'lastname')


mark = Astronaut()

mark.firstname = 'Mark'
mark.lastname = 'Watney'

mark.mission = 'Ares 3'
# Traceback (most recent call last):
# AttributeError: 'Astronaut' object has no attribute 'mission'

2.7.1. Example

class Astronaut:
    __slots__ = ()


mark = Astronaut()

mark.name = 'Mark Watney'
# Traceback (most recent call last):
# AttributeError: 'Astronaut' object has no attribute 'name'
class Astronaut:
    __slots__ = ('name',)


mark = Astronaut()

mark.name = 'Mark Watney'
mark.mission = 'Ares 3'
# Traceback (most recent call last):
# AttributeError: 'Astronaut' object has no attribute 'mission'

2.7.2. __slots__ and __dict__

  • Using __slots__ will prevent from creating __dict__

class Astronaut:
    __slots__ = ('name',)


mark = Astronaut()
mark.name = 'Mark Watney'

print(mark.__slots__)
# ('name',)

print(mark.__dict__)
# Traceback (most recent call last):
# AttributeError: 'Astronaut' object has no attribute '__dict__'
class Astronaut:
    __slots__ = ('__dict__', 'name')


mark = Astronaut()
mark.name = 'Mark Watney'   # will use __slots__
mark.mission = 'Ares 3'     # will use __dict__

print(mark.__slots__)
# ('__dict__', 'name')

print(mark.__dict__)
# {'mission': 'Ares 3'}

2.7.3. Slots and Methods

class Astronaut:
    __slots__ = ('name',)

    def say_hello(self):
        print(f'My name... {self.name}')


mark = Astronaut()
mark.name = 'Mark Watney'
mark.say_hello()

2.7.4. Slots and Init

class Astronaut:
    __slots__ = ('name',)

    def __init__(self, name)
        self.name = name


mark = Astronaut('Mark Watney')
print(mark.name)
# Mark Watney

2.7.5. Inheritance

  • Slots do not inherit, unless they are specified in subclass

  • Slots are added on inheritance

class Pilot:
    __slots__ = ('name',)

class Astronaut(Pilot):
    pass


mark = Astronaut()
mark.name = 'Mark Watney'
mark.mission = 'Ares 3'

print(mark.mission)
# Ares 3
class Pilot:
    __slots__ = ('name',)

class Astronaut(Pilot):
    __slots__ = ('name', 'mission')


mark = Astronaut()
mark.firstname = 'Mark Watney'
mark.mission = 'Ares 3'
mark.rank = 'Senior'
# Traceback (most recent call last):
# AttributeError: 'Astronaut' object has no attribute 'rank'
class Pilot:
    __slots__ = ('name',)


class Astronaut(Pilot):
    __slots__ = ('mission',)


mark = Astronaut()
mark.name = 'Mark Watney'
mark.mission = 'Ares 3'
mark.rank = 'Senior'
# Traceback (most recent call last):
# AttributeError: 'Astronaut' object has no attribute 'rank'

2.7.6. Use Case - 0x01

class Astronaut:
    __slots__ = ('firstname', 'lastname')


mark = Astronaut()
mark.firstname = 'Mark'
mark.lastname = 'Watney'

print(mark.firstname)
# Mark

print(mark.lastname)
# Watney

print(mark.__slots__)
# ('firstname', 'lastname')

print(mark.__dict__)
# Traceback (most recent call last):
# AttributeError: 'Astronaut' object has no attribute '__dict__'

result = {attr: getattr(mark, attr)
          for attr in mark.__slots__}

print(result)
# {'firstname': 'Mark', 'lastname': 'Watney'}

2.7.7. Assignments

Code 2.65. Solution
"""

* Assignment: OOP Slots Define
* Complexity: easy
* Lines of code: 11 lines
* Time: 13 min

English:
    1. Define class `Iris` with attributes: `sepal_length, sepal_width, petal_length, petal_width, species`
    2. All attributes must be in `__slots__`
    3. Define method `__repr__` which prints class name and all values positionally, ie. `Iris(5.8, 2.7, 5.1, 1.9, 'virginica')`
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Iris` z atrybutami: `sepal_length, sepal_width, petal_length, petal_width, species`
    2. Wszystkie atrybuty muszą być w `__slots__`
    3. Zdefiniuj metodę `__repr__` wypisującą nazwę klasy i wszystkie wartości atrybutów pozycyjnie, np. `Iris(5.8, 2.7, 5.1, 1.9, 'virginica')`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> result = [Iris(*row) for row in DATA[1:]]
    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [Iris(5.8, 2.7, 5.1, 1.9, 'virginica'),
     Iris(5.1, 3.5, 1.4, 0.2, 'setosa'),
     Iris(5.7, 2.8, 4.1, 1.3, 'versicolor'),
     Iris(6.3, 2.9, 5.6, 1.8, 'virginica'),
     Iris(6.4, 3.2, 4.5, 1.5, 'versicolor'),
     Iris(4.7, 3.2, 1.3, 0.2, 'setosa')]

    >>> iris = result[0]
    >>> iris
    Iris(5.8, 2.7, 5.1, 1.9, 'virginica')

    >>> iris.__slots__
    ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species')

    >>> [getattr(iris, x) for x in iris.__slots__]
    [5.8, 2.7, 5.1, 1.9, 'virginica']

    >>> {x: getattr(iris, x) for x in iris.__slots__}
    {'sepal_length': 5.8, 'sepal_width': 2.7, 'petal_length': 5.1, 'petal_width': 1.9, 'species': 'virginica'}

    >>> iris.__dict__
    Traceback (most recent call last):
    AttributeError: 'Iris' object has no attribute '__dict__'

    >>> values = tuple(getattr(iris, x) for x in iris.__slots__)
    >>> print(f'Iris{values}')
    Iris(5.8, 2.7, 5.1, 1.9, 'virginica')

Hint:
    * In `__repr__()` use tuple comprehension to get `self.__slots__` values
"""

DATA = [
    ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
    (5.8, 2.7, 5.1, 1.9, 'virginica'),
    (5.1, 3.5, 1.4, 0.2, 'setosa'),
    (5.7, 2.8, 4.1, 1.3, 'versicolor'),
    (6.3, 2.9, 5.6, 1.8, 'virginica'),
    (6.4, 3.2, 4.5, 1.5, 'versicolor'),
    (4.7, 3.2, 1.3, 0.2, 'setosa'),
]


# type: type
class Iris:
    ...