5.1. OOP 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
>>> class User:
... __slots__ = ('firstname', 'lastname')
...
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> mark.notexisting = True
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'notexisting'
When inheriting from a class without __slots__
, the __dict__
and __weakref__
attribute of the instances will always be accessible.
Without a __dict__
variable, instances cannot be assigned new variables
not listed in the __slots__
definition. Attempts to assign to an
unlisted variable name raises AttributeError. If dynamic assignment of new
variables is desired, then add '__dict__'
to the sequence of strings
in the __slots__
declaration.
Without a __weakref__
variable for each instance, classes defining
__slots__
do not support weak references to its instances. If weak
reference support is needed, then add '__weakref__'
to the sequence
of strings in the __slots__
declaration.
__slots__
are implemented at the class level by creating descriptors
for each variable name. As a result, class attributes cannot be used to set
default values for instance variables defined by __slots__
; otherwise,
the class attribute would overwrite the descriptor assignment.
The action of a __slots__
declaration is not limited to the class
where it is defined. __slots__
declared in parents are available in
child classes. However, child subclasses will get a __dict__
and
__weakref__
unless they also define __slots__
(which should only
contain names of any additional slots).
If a class defines a slot also defined in a base class, the instance variable defined by the base class slot is inaccessible (except by retrieving its descriptor directly from the base class). This renders the meaning of the program undefined. In the future, a check may be added to prevent this.
Nonempty __slots__
does not work for classes derived from
'variable-length'
built-in types such as int
, bytes
and
tuple
.
Any non-string iterable may be assigned to __slots__
.
If a dictionary is used to assign __slots__
, the dictionary keys will
be used as the slot names. The values of the dictionary can be used to
provide per-attribute docstrings that will be recognised by
inspect.getdoc()
and displayed in the output of help()
.
__class__
assignment works only if both classes have the same
__slots__
.
Multiple inheritance with multiple slotted parent classes can be used, but
only one parent is allowed to have attributes created by slots (the other
bases must have empty slot layouts) - violations raise TypeError
.
If an iterator is used for __slots__
then a descriptor is created for
each of the iterator's values. However, the __slots__
attribute will
be an empty iterator.
Source: [1]
5.1.1. Weakref¶
A weak reference to an object is not enough to keep the object alive: when the only remaining references to a referent are weak references, garbage collection is free to destroy the referent and reuse its memory for something else. However, until the object is actually destroyed the weak reference may return the object even if there are no strong references to it. A primary use for weak references is to implement caches or mappings holding large objects, where it's desired that a large object not be kept alive solely because it appears in a cache or mapping [3].
__weakref__
is just an opaque object that references all the weak
references to the current object. It's just an implementation detail that
allows the garbage collector to inform weak references that its referent
has been collected, and to not allow access to its underlying pointer
anymore. The weak reference can't rely on checking the reference count of
the object it refers to. This is because that memory may have been reclaimed
and is now being used by another object. Best case scenario the VM will
crash, worst case the weak reference will allow access to an object it
wasn't originally referring to. This is why the garbage collector must
inform the weak reference its referent is no longer valid. Weak references
form a stack. The top of that stack (the most recent weak reference to an
object) is available via __weakref__
. Weakrefs are re-used whenever
possible, so the stack is typically either empty or contains a single
element. [4]
Garbage collection is simply the process of freeing memory when it is not used/reached by any reference/pointer anymore. Python performs garbage collection via a technique called reference counting (and a cyclic garbage collector that is used to detect and break reference cycles). Using reference counting, GC collects the objects as soon as they become unreachable which happens when the number of references to the object is 0. [2]
The way with which weak references perform the task of NOT protecting the object from being collected by GC, or better to say the way with which they cause an object to be collected by GC is that (in case of a GC that uses reference counting rather than tracing technique) they just don't get to be counted as a reference. Otherwise, if counted, they will be called strong references [5].
5.1.2. Recap¶
>>> class User:
... pass
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> mark.email = 'mwatney@nasa.gov'
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney', 'email': 'mwatney@nasa.gov'}
>>> mark.__dict__
{'firstname': 'Mark', 'lastname': 'Watney', 'email': 'mwatney@nasa.gov'}
5.1.3. Declaration¶
>>> class User:
... __slots__ = ('firstname', 'lastname')
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> mark.email = 'mwatney@nasa.gov'
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'email'
>>> mark.phone = '+1 (234) 555 1337'
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'phone'
5.1.4. Get Value¶
>>> class User:
... __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> print(mark.firstname)
Mark
>>>
>>> print(mark.lastname)
Watney
5.1.5. Slots and Methods¶
>>> class User:
... __slots__ = ('firstname', 'lastname')
...
... def say_hello(self):
... print(f'My name... {self.firstname} {self.lastname}')
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> mark.say_hello()
My name... Mark Watney
5.1.6. Slots and Init¶
>>> class User:
... __slots__ = ('firstname', 'lastname')
...
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>>
>>> print(mark.firstname)
Mark
>>>
>>> print(mark.lastname)
Watney
Even inside of __init__
function you cannot assign to not slotted
attribute:
>>> class User:
... __slots__ = ('firstname', 'lastname')
...
... def __init__(self, firstname, lastname, email):
... self.firstname = firstname
... self.lastname = lastname
... self.email = email
>>>
>>>
>>> mark = User('Mark', 'Watney', 'mwatney@nasa.gov')
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'email'
5.1.7. Vars¶
Using
__slots__
will prevent from creating__dict__
>>> class User:
... __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
Builtin function vars()
will not work on slots:
>>> vars(mark)
Traceback (most recent call last):
TypeError: vars() argument must have __dict__ attribute
This is because vars()
display content of __dict__
:
>>> print(mark.__dict__)
Traceback (most recent call last):
AttributeError: 'User' object has no attribute '__dict__'
5.1.8. Slots vs Attributes¶
>>> class User:
... pass
...
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': None})
>>> class User:
... firstname = 'Mark'
... lastname = 'Watney'
...
>>> vars(User)
mappingproxy({'__module__': '__main__',
'firstname': 'Mark',
'lastname': 'Watney',
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': None})
>>> class User:
... __slots__ = ('firstname', 'lastname')
...
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__slots__': ('firstname', 'lastname'),
'firstname': <member 'firstname' of 'User' objects>,
'lastname': <member 'lastname' of 'User' objects>,
'__doc__': None})
5.1.9. Slots Internals¶
Slots are descriptors
>>> class User:
... __slots__ = ('firstname', 'lastname')
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__slots__': ('firstname', 'lastname'),
'firstname': <member 'firstname' of 'User' objects>,
'lastname': <member 'lastname' of 'User' objects>,
'__doc__': None})
>>> User.firstname
<member 'firstname' of 'User' objects>
>>>
>>> type(User.firstname)
<class 'member_descriptor'>
5.1.10. Slots Dict¶
Docstring for slotted names
Used for documentation
If a dictionary is used to assign __slots__
, the dictionary keys will
be used as the slot names. The values of the dictionary can be used to
provide per-attribute docstrings that will be recognised by
inspect.getdoc()
and displayed in the output of help()
.
>>> class User:
... __slots__ = {
... 'firstname': 'Docstring for firstname attribute',
... 'lastname': 'Docstring for lastname attribute',
... }
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__slots__': {'firstname': 'Docstring for firstname attribute',
'lastname': 'Docstring for lastname attribute'},
'firstname': <member 'firstname' of 'User' objects>,
'lastname': <member 'lastname' of 'User' objects>,
'__doc__': None})
5.1.11. Get Attributes and Values¶
To get values iterate over
self.__slots__
and usegetattr(self, x)
>>> class User:
... __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> print(mark.__slots__)
('firstname', 'lastname')
>>> {x: getattr(mark, x) for x in mark.__slots__}
{'firstname': 'Mark', 'lastname': 'Watney'}
5.1.12. Slots and Dunder Dict¶
Using
__slots__
will prevent from creating__dict__
Adding
__dict__
to__slots__
will combine both worlds
>>> class User:
... __slots__ = ('__dict__', 'firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark' # will use __slots__
>>> mark.lastname = 'Watney' # will use __slots__
>>> mark.email = 'mwatney@nasa.gov' # will use __dict__
>>> mark.phone = '+1 (234) 567-8910' # will use __dict__
>>> print(mark.__slots__)
('__dict__', 'firstname', 'lastname')
>>> vars(mark)
{'email': 'mwatney@nasa.gov', 'phone': '+1 (234) 567-8910'}
>>> slots = {x:getattr(mark, x) for x in mark.__slots__ if x != '__dict__'}
>>> slots | vars(mark)
{'firstname': 'Mark',
'lastname': 'Watney',
'email': 'mwatney@nasa.gov',
'phone': '+1 (234) 567-8910'}
5.1.13. 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 Account:
... __slots__ = ('username', 'password')
>>>
>>> class User(Account):
... pass
>>>
>>>
>>> mark = User()
>>> mark.username = 'mwatney'
>>> mark.password = 'Ares3'
>>> mark.groups = ['user', 'staff', 'admin']
>>>
>>>
>>> print(mark.username)
mwatney
>>>
>>> print(mark.password)
Ares3
>>>
>>> print(mark.groups)
['user', 'staff', 'admin']
>>>
>>>
>>> vars(mark)
{'groups': ['user', 'staff', 'admin']}
>>>
>>>
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': None})
>>> class Account:
... __slots__ = ('username', 'password')
>>>
>>> class User(Account):
... __slots__ = ()
>>>
>>>
>>> mark = User()
>>> mark.username = 'mwatney'
>>> mark.password = 'Ares3'
>>> mark.groups = ['user', 'staff', 'admin']
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'groups'
>>>
>>>
>>> vars(mark)
Traceback (most recent call last):
TypeError: vars() argument must have __dict__ attribute
>>>
>>> vars(User)
mappingproxy({'__module__': '__main__', '__slots__': (), '__doc__': None})
>>> class Account:
... __slots__ = ('username', 'password')
>>>
>>> class User(Account):
... __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>> mark.username = 'mwatney'
>>> mark.password = 'Ares3'
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> mark.groups = ['user', 'staff', 'admin']
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'groups'
>>>
>>>
>>> vars(mark)
Traceback (most recent call last):
TypeError: vars() argument must have __dict__ attribute
>>>
>>> vars(Account)
mappingproxy({'__module__': '__main__',
'__slots__': ('username', 'password'),
'password': <member 'password' of 'Account' objects>,
'username': <member 'username' of 'Account' objects>,
'__doc__': None})
>>>
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__slots__': ('firstname', 'lastname'),
'firstname': <member 'firstname' of 'User' objects>,
'lastname': <member 'lastname' of 'User' objects>,
'__doc__': None})
5.1.14. Change Slots¶
>>> class User:
... __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>>
>>> mark.__slots__
('firstname', 'lastname')
>>>
>>> mark.__slots__ = ('myslot1', 'myslot2')
Traceback (most recent call last):
AttributeError: 'User' object attribute '__slots__' is read-only
>>> class User:
... __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>>
>>> User.__slots__ = ('myslot1', 'myslot2')
>>> User.__slots__
('myslot1', 'myslot2')
>>>
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__slots__': ('myslot1', 'myslot2'),
'firstname': <member 'firstname' of 'User' objects>,
'lastname': <member 'lastname' of 'User' objects>,
'__doc__': None})
>>> class User:
... __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>>
>>> User.__slots__ = ('myslot1', 'myslot2')
>>> User.__slots__
('myslot1', 'myslot2')
>>>
>>>
>>> User.firstname
<member 'firstname' of 'User' objects>
>>>
>>> User.lastname
<member 'lastname' of 'User' objects>
>>>
>>> User.myslot1
Traceback (most recent call last):
AttributeError: type object 'User' has no attribute 'myslot1'
>>>
>>> User.myslot2
Traceback (most recent call last):
AttributeError: type object 'User' has no attribute 'myslot2'
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.myslot1
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'myslot1'
>>>
>>> mark.myslot2
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'myslot2'
5.1.15. Slots in Dataclasses¶
Since Python 3.10
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass(slots=True)
... class Iris:
... sl: float
... sw: float
... pl: float
... pw: float
... species: str
5.1.16. Use Case - 0x01¶
>>> from dataclasses import dataclass
>>> from itertools import starmap
>>>
>>>
>>> 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'),
... (7.0, 3.2, 4.7, 1.4, 'versicolor'),
... (7.6, 3.0, 6.6, 2.1, 'virginica'),
... (4.9, 3.0, 1.4, 0.2, 'setosa'),
... (4.9, 2.5, 4.5, 1.7, 'virginica'),
... (7.1, 3.0, 5.9, 2.1, 'virginica'),
... (4.6, 3.4, 1.4, 0.3, 'setosa'),
... (5.4, 3.9, 1.7, 0.4, 'setosa'),
... (5.7, 2.8, 4.5, 1.3, 'versicolor'),
... (5.0, 3.6, 1.4, 0.3, 'setosa'),
... (5.5, 2.3, 4.0, 1.3, 'versicolor'),
... (6.5, 3.0, 5.8, 2.2, 'virginica'),
... (6.5, 2.8, 4.6, 1.5, 'versicolor'),
... (6.3, 3.3, 6.0, 2.5, 'virginica'),
... (6.9, 3.1, 4.9, 1.5, 'versicolor'),
... (4.6, 3.1, 1.5, 0.2, 'setosa'),
... ]
>>>
>>> @dataclass(slots=True, frozen=True)
... class Iris:
... sl: float
... sw: float
... pl: float
... pw: float
... species: str
>>>
>>>
>>> result = starmap(Iris, DATA[1:])
>>>
>>> list(result)
[Iris(sl=5.8, sw=2.7, pl=5.1, pw=1.9, species='virginica'),
Iris(sl=5.1, sw=3.5, pl=1.4, pw=0.2, species='setosa'),
Iris(sl=5.7, sw=2.8, pl=4.1, pw=1.3, species='versicolor'),
Iris(sl=6.3, sw=2.9, pl=5.6, pw=1.8, species='virginica'),
Iris(sl=6.4, sw=3.2, pl=4.5, pw=1.5, species='versicolor'),
Iris(sl=4.7, sw=3.2, pl=1.3, pw=0.2, species='setosa'),
Iris(sl=7.0, sw=3.2, pl=4.7, pw=1.4, species='versicolor'),
Iris(sl=7.6, sw=3.0, pl=6.6, pw=2.1, species='virginica'),
Iris(sl=4.9, sw=3.0, pl=1.4, pw=0.2, species='setosa'),
Iris(sl=4.9, sw=2.5, pl=4.5, pw=1.7, species='virginica'),
Iris(sl=7.1, sw=3.0, pl=5.9, pw=2.1, species='virginica'),
Iris(sl=4.6, sw=3.4, pl=1.4, pw=0.3, species='setosa'),
Iris(sl=5.4, sw=3.9, pl=1.7, pw=0.4, species='setosa'),
Iris(sl=5.7, sw=2.8, pl=4.5, pw=1.3, species='versicolor'),
Iris(sl=5.0, sw=3.6, pl=1.4, pw=0.3, species='setosa'),
Iris(sl=5.5, sw=2.3, pl=4.0, pw=1.3, species='versicolor'),
Iris(sl=6.5, sw=3.0, pl=5.8, pw=2.2, species='virginica'),
Iris(sl=6.5, sw=2.8, pl=4.6, pw=1.5, species='versicolor'),
Iris(sl=6.3, sw=3.3, pl=6.0, pw=2.5, species='virginica'),
Iris(sl=6.9, sw=3.1, pl=4.9, pw=1.5, species='versicolor'),
Iris(sl=4.6, sw=3.1, pl=1.5, pw=0.2, species='setosa')]
5.1.17. Use Case - 0x02¶
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)
Test:
>>> class User:
... __slots__ = ('firstname', 'lastname')
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>>
>>> deepsizeof(mark)
156
>>> class User:
... pass
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>>
>>> deepsizeof(mark)
575
5.1.18. Further Reading¶
5.1.19. References¶
5.1.20. Assignments¶
"""
* Assignment: OOP AttributeSlots Dataclass
* Complexity: easy
* Lines of code: 4 lines
* Time: 3 min
English:
1. Define dataclass `User` with slots:
a. `firstname: str`
b. `lastname: str`
2. Run doctests - all must succeed
Polish:
1. Zdefiniuj dataklasę `User` ze slotami:
a. `firstname: str`
b. `lastname: str`
2. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from dataclasses import is_dataclass
>>> assert hasattr(User, '__slots__')
>>> assert 'firstname' in User.__slots__
>>> assert 'lastname' in User.__slots__
>>> assert User is not Ellipsis, \
'Assign result to variable: `User`'
>>> assert type(User) is type, \
'Result must be a type'
>>> assert is_dataclass(User), \
'Class User has to be dataclass'
>>> result = User(firstname='Mark', lastname='Watney')
>>> assert not hasattr(result, '__dict__')
>>> assert not hasattr(result, '__weakref__')
"""
from dataclasses import dataclass
# Define dataclass `User` with slots:
# - `firstname: str`
# - `lastname: str`
# type: type[User]
@dataclass
class User:
...
"""
* Assignment: OOP AttributeSlots Define
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min
English:
1. Define class `User` with slots:
a. `firstname: str`
b. `lastname: str`
2. Do not define `__init__()` method
3. Do not use dataclass
4. Run doctests - all must succeed
Polish:
1. Zdefiniuj klasę `User` ze slotami:
a. `firstname: str`
b. `lastname: str`
2. Nie definiuj metody `__init__()`
3. Nie używaj dataclass
4. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from dataclasses import is_dataclass
>>> assert User is not Ellipsis, \
'Assign result to variable: `User`'
>>> assert type(User) is type, \
'Result must be a type'
>>> assert not is_dataclass(User), \
'Class User cannot be dataclass'
>>> assert hasattr(User, '__slots__')
>>> assert 'firstname' in User.__slots__
>>> assert 'lastname' in User.__slots__
>>> result = User()
>>> assert not hasattr(result, '__dict__')
>>> assert not hasattr(result, '__weakref__')
"""
# Define class `User` with slots:
# - `firstname: str`
# - `lastname: str`
# Do not define `__init__()` method
# Do not use dataclass
# type: type[User]
class User:
...
"""
* Assignment: OOP AttributeSlots Init
* Complexity: easy
* Lines of code: 3 lines
* Time: 2 min
English:
1. Define class `User` with slots:
a. `firstname: str`
b. `lastname: str`
2. Define `__init__()` method
3. Do not use dataclass
4. Run doctests - all must succeed
Polish:
1. Zdefiniuj klasę `User` z slotami:
a. `firstname: str`
b. `lastname: str`
2. Zdefiniuj metodę `__init__()`
3. Nie używaj dataclass
4. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from dataclasses import is_dataclass
>>> assert hasattr(User, '__slots__')
>>> assert 'firstname' in User.__slots__
>>> assert 'lastname' in User.__slots__
>>> assert User is not Ellipsis, \
'Assign result to variable: `User`'
>>> assert type(User) is type, \
'Result must be a type'
>>> assert not is_dataclass(User), \
'Class User cannot be dataclass'
>>> result = User(firstname='Mark', lastname='Watney')
>>> assert not hasattr(result, '__dict__')
>>> assert not hasattr(result, '__weakref__')
"""
# Define class `User` with slots:
# - `firstname: str`
# - `lastname: str`
# Define `__init__()` method
# type: type[User]
class User:
__slots__ = ('firstname', 'lastname')
"""
* Assignment: OOP AttributeSlots Init
* Complexity: easy
* Lines of code: 2 lines
* Time: 3 min
English:
1. Define function `dump(obj) -> dict` accepting instance with slots
2. Function should return similar output to `vars()`, i.e.:
{'firstname':'mwatney', 'lastname':'Ares3'}
3. Run doctests - all must succeed
Polish:
1. Zdefiniuj funkcję `dump(obj) -> dict` przyjmującą instancję ze slotami
2. Funkcja powinna zwracać podobny wynik do `vars()`, np:
{'firstname':'mwatney', 'lastname':'Ares3'}
3. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from dataclasses import is_dataclass
>>> class User:
... __slots__ = ('firstname', 'lastname')
...
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
>>>
>>>
>>> mark = User(firstname='Mark', lastname='Watney')
>>> result = dump(mark)
>>> assert result is not Ellipsis, \
'Assign result to variable: `result`'
>>> assert type(result) is dict, \
'Result must be a type'
>>> assert len(result) == 2, \
'Result length must be 2'
>>> assert all(type(x) is str for x in result.keys()), \
'All keys in result must be a str'
>>> assert all(type(x) is str for x in result.values()), \
'All values in result must be a str'
>>> result
{'firstname': 'Mark', 'lastname': 'Watney'}
"""
# Define function `dump(obj) -> dict` accepting instance with slots
# Function should return similar output to `vars()`
# type: Callable[[object], dict]
def dump(obj) -> dict:
...
"""
* Assignment: OOP AttributeSlots Repr
* Complexity: medium
* Lines of code: 4 lines
* Time: 5 min
English:
1. Define method `__repr__` which prints class name and all values
positionally, ie. `Iris(5.8, 2.7, 5.1, 1.9, 'virginica')`
2. Run doctests - all must succeed
Polish:
1. 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')`
2. 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__'. Did you mean: '__dir__'?
>>> 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:
__slots__ = ('sepal_length', 'sepal_width', 'petal_length',
'petal_width', 'species')
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
def _dump(self) -> dict:
return {attrname: getattr(self, attrname)
for attrname in self.__slots__}
# Define method `__repr__` which prints class name and all values
# positionally, ie. `Iris(5.8, 2.7, 5.1, 1.9, 'virginica')`
# type: Callable[[Self], str]
def __repr__(self):
...