5.14. OOP Slots

5.14.1. Rationale

  • 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__

5.14.2. Example

>>> class Astronaut:
...     __slots__ = ()
>>>
>>>
>>> astro = Astronaut()
>>>
>>> astro.fullname = 'Mark Watney'
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'fullname'
>>> class Astronaut:
...     __slots__ = ('fullname',)
>>>
>>>
>>> astro = Astronaut()
>>>
>>> astro.fullname = 'Mark Watney'
>>> astro.role = 'Botanist'
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'role'
>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> astro = Astronaut()
>>>
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'
>>> astro.role = 'Botanist'
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'role'

5.14.3. Get Value

>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'
>>>
>>> print(astro.firstname)
Mark
>>> print(astro.lastname)
Watney

5.14.4. Slots and Methods

>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
...
...     def say_hello(self):
...         print(f'My name... {self.firstname} {self.lastname}')
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'
>>>
>>> astro.say_hello()
My name... Mark Watney

5.14.5. Slots and Init

>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
...
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> print(astro.firstname)
Mark
>>> print(astro.lastname)
Watney
>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
...
...     def __init__(self, firstname, lastname, role):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.role = role
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney', 'Botanist')
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'role'

5.14.6. Get Vars

  • Using __slots__ will prevent from creating __dict__

>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'
>>>
>>> vars(astro)
Traceback (most recent call last):
TypeError: vars() argument must have __dict__ attribute
>>>
>>> print(astro.__dict__)
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute '__dict__'
>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'
>>>
>>> print(astro.__slots__)
('firstname', 'lastname')
>>>
>>> {x: getattr(astro, x) for x in astro.__slots__}
{'firstname': 'Mark', 'lastname': 'Watney'}
>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> vars(Astronaut)  
mappingproxy({'__module__': 'builtins',
              '__slots__': ('firstname', 'lastname'),
              'firstname': <member 'firstname' of 'Astronaut' objects>,
              'lastname': <member 'lastname' of 'Astronaut' objects>,
              '__doc__': None})
>>>
>>> Astronaut.firstname
<member 'firstname' of 'Astronaut' objects>
>>>
>>> type(Astronaut.firstname)
<class 'member_descriptor'>

5.14.7. Slots and Dict

  • Using __slots__ will prevent from creating __dict__

  • Adding __dict__ to __slots__ will combine both worlds

>>> class Astronaut:
...     __slots__ = ('__dict__', 'firstname', 'lastname')
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'   # will use __slots__
>>> astro.lastname = 'Watney'  # will use __slots__
>>> astro.role = 'Botanist'    # will use __dict__
>>> astro.mission = 'Ares3'    # will use __dict__
>>>
>>> print(astro.__slots__)
('__dict__', 'firstname', 'lastname')
>>>
>>> vars(astro)
{'role': 'Botanist', 'mission': 'Ares3'}
>>>
>>> {x:getattr(astro, x) for x in astro.__slots__ if x != '__dict__'} | vars(astro)  
{'firstname': 'Mark',
 'lastname': 'Watney',
 'role': 'Botanist',
 'mission': 'Ares3'}

5.14.8. Inheritance

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

  • Slots are added on inheritance

  • If class does not specify slots, the __dict__ will be added

>>> class Person:
...     __slots__ = ('firstname', 'lastname')
>>>
>>> class Astronaut(Person):
...     pass
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'
>>> astro.role = 'Botanist'
>>>
>>> print(astro.firstname)
Mark
>>> print(astro.lastname)
Watney
>>> print(astro.role)
Botanist
>>>
>>>
>>> vars(astro)
{'role': 'Botanist'}
>>>
>>> vars(Astronaut)  
mappingproxy({'__module__': 'builtins',
              '__dict__': <attribute '__dict__' of 'Astronaut' objects>,
              '__weakref__': <attribute '__weakref__' of 'Astronaut' objects>,
              '__doc__': None})
>>> class Person:
...     __slots__ = ('firstname', 'lastname')
>>>
>>> class Astronaut(Person):
...     __slots__ = ()
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'
>>> astro.role = 'Botanist'
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'role'
>>>
>>>
>>> vars(astro)
Traceback (most recent call last):
TypeError: vars() argument must have __dict__ attribute
>>>
>>> vars(Astronaut)
mappingproxy({'__module__': 'builtins', '__slots__': (), '__doc__': None})
>>> class Person:
...     __slots__ = ('firstname', 'lastname')
>>>
>>> class Astronaut(Person):
...     __slots__ = ('role',)
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'
>>> astro.role = 'Botanist'
>>> astro.agency = 'NASA'
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'agency'
>>>
>>>
>>> vars(astro)
Traceback (most recent call last):
TypeError: vars() argument must have __dict__ attribute
>>>
>>> vars(Person)  
mappingproxy({'__module__': 'builtins',
              '__slots__': ('firstname', 'lastname'),
              'firstname': <member 'firstname' of 'Person' objects>,
              'lastname': <member 'lastname' of 'Person' objects>,
              '__doc__': None})
>>>
>>> vars(Astronaut)  
mappingproxy({'__module__': 'builtins',
              '__slots__': ('role',),
              'role': <member 'role' of 'Astronaut' objects>,
              '__doc__': None})

5.14.9. Change Slots

>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> astro = Astronaut()
>>>
>>> astro.__slots__
('firstname', 'lastname')
>>>
>>> astro.__slots__ = ('myslot1', 'myslot2')
Traceback (most recent call last):
AttributeError: 'Astronaut' object attribute '__slots__' is read-only
>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> astro = Astronaut()
>>>
>>> Astronaut.__slots__ = ('myslot1', 'myslot2')
>>> Astronaut.__slots__
('myslot1', 'myslot2')
>>>
>>> vars(Astronaut)  
mappingproxy({'__module__': 'builtins',
              '__slots__': ('myslot1', 'myslot2'),
              'firstname': <member 'firstname' of 'Astronaut' objects>,
              'lastname': <member 'lastname' of 'Astronaut' objects>,
              '__doc__': None})
>>> class Astronaut:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> astro = Astronaut()
>>> astro.firstname = 'Mark'
>>> astro.lastname = 'Watney'
>>>
>>> Astronaut.__slots__ = ('myslot1', 'myslot2')
>>> Astronaut.__slots__
('myslot1', 'myslot2')
>>>
>>>
>>> Astronaut.firstname
<member 'firstname' of 'Astronaut' objects>
>>>
>>> Astronaut.lastname
<member 'lastname' of 'Astronaut' objects>
>>>
>>> Astronaut.myslot1
Traceback (most recent call last):
AttributeError: type object 'Astronaut' has no attribute 'myslot1'
>>>
>>> Astronaut.myslot2
Traceback (most recent call last):
AttributeError: type object 'Astronaut' has no attribute 'myslot2'
>>>
>>> astro.firstname
'Mark'
>>>
>>> astro.lastname
'Watney'
>>>
>>> astro.myslot1
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'myslot1'
>>>
>>> astro.myslot2
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'myslot2'

5.14.10. Use Case - Deep Size

>>> from sys import getsizeof
>>> from itertools import chain
>>> from collections import deque
>>> import logging
>>>
>>>
>>> logging.basicConfig(level='DEBUG')
>>> log = logging.getLogger('deepsizeof')
>>>
>>>
>>> def deepsizeof(o, handlers={}):
...     """
...     Returns the approximate memory footprint an object and all of its contents.
...
...     Automatically finds the contents of the following builtin containers and
...     their subclasses: tuple, list, deque, dict, set and frozenset
...     """
...     dict_handler = lambda d: chain.from_iterable(d.items())
...     all_handlers = {tuple: iter,
...                     list: iter,
...                     deque: iter,
...                     dict: dict_handler,
...                     set: iter,
...                     frozenset: iter}
...     all_handlers.update(handlers)     # user handlers take precedence
...     seen = set()                      # track which object id's have already been seen
...     default_size = getsizeof(0)       # estimate sizeof object without __sizeof__
...
...     def sizeof(o):
...         if id(o) in seen:       # do not double count the same object
...             return 0
...         seen.add(id(o))
...         s = getsizeof(o, default_size)
...
...         log.debug('Size: %s, Type: %s, Repr: %s', s, type(o), repr(o))
...
...         for typ, handler in all_handlers.items():
...             if isinstance(o, typ):
...                 s += sum(map(sizeof, handler(o)))
...                 break
...         else:
...             if not hasattr(o.__class__, '__slots__'):
...                 if hasattr(o, '__dict__'):
...                     # no __slots__ *usually* means a
...                     # __dict__, but some special builtin classes (such
...                     # as `type(None)`) have neither
...                     # else, `o` has no attributes at all, so sys.getsizeof()
...                     # actually returned the correct value
...                     s += sizeof(o.__dict__)
...             else:
...                 s += sum(
...                     sizeof(getattr(o, x))
...                            for x in o.__class__.__slots__
...                            if hasattr(o, x))
...         return s
...     return sizeof(o)
>>>
>>>
>>> 
... if __name__ == 'builtins':
...     class Astronaut:
...        __slots__ = ('firstname', 'lastname')
...
...     class Cosmonaut:
...         pass
...
...     a = Astronaut()
...     a.firstname = 'Mark'
...     a.lastname = 'Watney'
...
...     c = Cosmonaut()
...     c.firstname = 'Mark'
...     c.lastname = 'Watney'
...
...     print('Astronaut', deepsizeof(a))
...     print('Cosmonaut', deepsizeof(c))
DEBUG:deepsizeof:Size: 48, Type: <class 'Astronaut'>, Repr: <Astronaut object at 0x10790b940>
DEBUG:deepsizeof:Size: 53, Type: <class 'str'>, Repr: 'Mark'
DEBUG:deepsizeof:Size: 55, Type: <class 'str'>, Repr: 'Watney'
DEBUG:deepsizeof:Size: 48, Type: <class 'Cosmonaut'>, Repr: <Cosmonaut object at 0x10790b9d0>
DEBUG:deepsizeof:Size: 104, Type: <class 'dict'>, Repr: {'firstname': 'Mark', 'lastname': 'Watney'}
DEBUG:deepsizeof:Size: 58, Type: <class 'str'>, Repr: 'firstname'
DEBUG:deepsizeof:Size: 53, Type: <class 'str'>, Repr: 'Mark'
DEBUG:deepsizeof:Size: 57, Type: <class 'str'>, Repr: 'lastname'
DEBUG:deepsizeof:Size: 55, Type: <class 'str'>, Repr: 'Watney'
Astronaut 156
Cosmonaut 375

5.14.11. Assignments

Code 5.53. Solution
"""
* Assignment: OOP Slots Define
* Complexity: easy
* Lines of code: 4 lines
* Time: 8 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__}  # doctest: +NORMALIZE_WHITESPACE
    {'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')]


class Iris:
    def __init__(self, sepal_length, sepal_width, petal_length, petal_width, species):
        self.sepal_length = sepal_length
        self.sepal_width = sepal_width
        self.petal_length = petal_length
        self.petal_width = petal_width
        self.species = species