2.5. Slots

2.5.1. Rationale

  • Faster attribute access

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

  • Prevents from adding new attibutes

  • 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')


astro = Astronaut()

astro.firstname = 'Mark'
astro.lastname = 'Watney'

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

2.5.2. Example

class Astronaut:
    __slots__ = ()


astro = Astronaut()

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


astro = Astronaut()

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

2.5.3. __slots__ and __dict__

  • Using __slots__ will prevent from creating __dict__

class Astronaut:
    __slots__ = ('name',)


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

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

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


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

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

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

2.5.4. Slots and Methods

class Astronaut:
    __slots__ = ('name',)

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


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

2.5.5. Slots and Init

class Astronaut:
    __slots__ = ('name',)

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


astro = Astronaut('Mark Watney')
print(astro.name)
# Mark Watney
class Astronaut:
    __slots__ = ('name',)

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


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

2.5.6. Inheritance

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

  • Slots are added on inheritance

class Pilot:
    __slots__ = ('name',)

class Astronaut(Pilot):
    pass


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

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

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


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


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


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

2.5.7. Use Cases

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


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

print(astro.firstname)
# Mark

print(astro.lastname)
# Watney

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

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

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

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

2.5.8. Assignments

2.5.8.1. OOP Slots Define

  • Assignment name: OOP Slots Define

  • Last update: 2020-10-02

  • Complexity level: easy

  • Lines of code to write: 11 lines

  • Estimated time of completion: 13 min

  • Solution: solution/oop_slots_define.py

English
  1. Use code from "Input" section (see below)

  2. Define class Iris with attributes: sepal_length, sepal_width, petal_length, petal_width, species

  3. All attributes must be in __slots__

  4. Define method __repr__ which prints class name and all values positionally, ie. Iris(5.8, 2.7, 5.1, 1.9, 'virginica')

  5. Compare result with "Output" section (see below)

Polish
  1. Użyj kodu z sekcji "Input" (patrz poniżej)

  2. Zdefiniuj klasę Iris z atrybutami: sepal_length, sepal_width, petal_length, petal_width, species

  3. Wszystkie atrybuty muszą być w __slots__

  4. 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')

  5. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Input
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'),
]
Output
>>> 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