4.6. Dataclass Mutable Attrs

4.6.1. SetUp

>>> from dataclasses import dataclass, field

4.6.2. Problem

Note, You should not set mutable objects as a default function argument. More information in Argument Mutability. This is how all dynamically typed languages work (including PHP, Ruby, Perl etc).

>>> class User:
...     def __init__(self, name, groups=[]):
...         self.name = name
...         self.groups = groups
>>>
>>>
>>> mark = User('Mark Watney')
>>> melissa = User('Melissa Lewis')
>>>
>>> mark.groups.append('user')
>>> mark.groups.append('staff')
>>> mark.groups.append('admin')
>>>
>>> print(f'Name: {mark.name}, Missions: {mark.groups}')
Name: Mark Watney, Missions: ['user', 'staff', 'admin']
>>>
>>> print(f'Name: {melissa.name}, Missions: {melissa.groups}')
Name: Melissa Lewis, Missions: ['user', 'staff', 'admin']
>>> class User:
...     def __init__(self, name, groups=None):
...         self.name = name
...         self.groups = groups if groups else []
>>>
>>>
>>> mark = User('Mark Watney')
>>> melissa = User('Melissa Lewis')
>>>
>>> mark.groups.append('user')
>>> mark.groups.append('staff')
>>> mark.groups.append('admin')
>>>
>>> print(f'Name: {mark.name}, Missions: {mark.groups}')
Name: Mark Watney, Missions: ['user', 'staff', 'admin']
>>>
>>> print(f'Name: {melissa.name}, Missions: {melissa.groups}')
Name: Melissa Lewis, Missions: []

4.6.3. List of Strings

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[str] = field(default_factory=list)
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> print(mark)
User(firstname='Mark', lastname='Watney', groups=[])

4.6.4. List of Objects

>>> @dataclass
... class Group:
...     gid: int
...     name: str
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[Group] = field(default_factory=list)
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> print(mark)
User(firstname='Mark', lastname='Watney', groups=[])

4.6.5. Dict

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: dict[int,str] = field(default_factory=dict)
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> print(mark)
User(firstname='Mark', lastname='Watney', groups={})

4.6.6. Default Values

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[str] = field(default_factory=lambda: ['user', 'staff', 'admins'])
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> print(mark)
User(firstname='Mark', lastname='Watney', groups=['user', 'staff', 'admins'])

4.6.7. Assignments

Code 4.36. Solution
"""
* Assignment: Dataclass Mutability list
* Complexity: easy
* Lines of code: 3 lines
* Time: 3 min

English:
    1. Create dataclass `User`, with attributes:
        a. `firstname: str` (required)
        b. `lastname: str` (required)
        c. `groups: list[str]` (optional)
    2. Attributes must be set at the initialization from constructor arguments
    3. Avoid mutable parameter problem
    4. Run doctests - all must succeed

Polish:
    1. Stwórz dataklasę `User`, z atrybutami:
        a. `firstname: str` (wymagane)
        b. `lastname: str` (wymagane)
        c. `groups: list[str]` (opcjonalne)
    2. Atrybuty mają być ustawiane przy inicjalizacji z parametrów konstruktora
    3. Uniknij problemu mutowalnych parametrów
    4. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * field(default_factory=list)

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(User)
    >>> assert hasattr(User, '__annotations__')

    >>> assert 'firstname' in User.__dataclass_fields__
    >>> assert 'lastname' in User.__dataclass_fields__
    >>> assert 'groups' in User.__dataclass_fields__

    >>> mark = User('mwatney', 'Ares3')
    >>> assert mark.firstname == 'mwatney'
    >>> assert mark.lastname == 'Ares3'
    >>> assert mark.groups == []

    >>> melissa = User('mlewis', 'Nasa1', groups=['user', 'staff', 'admin'])
    >>> assert melissa.firstname == 'mlewis'
    >>> assert melissa.lastname == 'Nasa1'
    >>> assert melissa.groups == ['user', 'staff', 'admin']

    >>> assert id(mark.groups) != id(melissa.groups)
"""
from dataclasses import dataclass, field


# Create class `User`, with attributes:
# - `firstname: str` (required)
# - `lastname: str` (required)
# - `groups: list[str]` (optional)
# type: type[User]
@dataclass
class User:
    ...


Code 4.37. Solution
"""
* Assignment: Dataclass Mutability list[str]
* Complexity: easy
* Lines of code: 3 lines
* Time: 3 min

English:
    1. Create dataclass `User`, with attributes:
        a. `firstname: str` (required)
        b. `lastname: str` (required)
        c. `groups: list[str]` (optional), default: ['user']
    2. Attributes must be set at the initialization from constructor arguments
    3. Avoid mutable parameter problem
    4. Run doctests - all must succeed

Polish:
    1. Stwórz dataklasę `User`, z atrybutami:
        a. `firstname: str` (wymagane)
        b. `lastname: str` (wymagane)
        c. `groups: list[str]` (opcjonalne), domyślnie: ['user']
    2. Atrybuty mają być ustawiane przy inicjalizacji z parametrów konstruktora
    3. Uniknij problemu mutowalnych parametrów
    4. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * field(default_factory=lambda: [...])

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(User)
    >>> assert hasattr(User, '__annotations__')

    >>> assert 'firstname' in User.__dataclass_fields__
    >>> assert 'lastname' in User.__dataclass_fields__
    >>> assert 'groups' in User.__dataclass_fields__

    >>> mark = User('mwatney', 'Ares3')
    >>> assert mark.firstname == 'mwatney'
    >>> assert mark.lastname == 'Ares3'
    >>> assert mark.groups == ['user']

    >>> melissa = User('mlewis', 'Nasa1', groups=['user', 'staff', 'admin'])
    >>> assert melissa.firstname == 'mlewis'
    >>> assert melissa.lastname == 'Nasa1'
    >>> assert melissa.groups == ['user', 'staff', 'admin']

    >>> assert id(mark.groups) != id(melissa.groups)
"""
from dataclasses import dataclass, field


# Create dataclass `User`, with attributes:
# - `firstname: str` (required)
# - `lastname: str` (required)
# - `groups: list[str]` (optional), default: ['user']
# type: type[User]
@dataclass
class User:
    ...


Code 4.38. Solution
"""
* Assignment: OOP AttributeMutability Randint Dataclass
* Complexity: easy
* Lines of code: 3 lines
* Time: 3 min

English:
    1. Create class `Hero`, with attributes:
        a. `name: str` (required)
        b. `health: int` (optional), default: randint(50, 100)
    2. Attributes must be set at the initialization from constructor arguments
    3. Avoid mutable parameter problem
    4. Użyj funkcji `randint()` z biblioteki `random`
    5. Use `dataclass`
    6. Run doctests - all must succeed

Polish:
    1. Stwórz klasę `Hero`, z atrybutami:
        a. `name: str` (wymagane)
        b. `health: int` (opcjonalne), domyślnie: randint(50, 100)
    2. Atrybuty mają być ustawiane przy inicjalizacji z parametrów konstruktora
    3. Uniknij problemu mutowalnych parametrów
    4. Użyj funkcji `randint()` z biblioteki `random`
    5. Użyj `dataclass`
    6. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * field(default_factory=lambda:...)

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass
    >>> from random import seed; seed(0)

    >>> assert isclass(Hero)
    >>> assert hasattr(Hero, '__annotations__')

    >>> assert 'name' in Hero.__dataclass_fields__
    >>> assert 'health' in Hero.__dataclass_fields__

    >>> warrior = Hero('Warrior')
    >>> assert warrior.name == 'Warrior'
    >>> assert warrior.health == 74

    >>> mage = Hero('Mage')
    >>> assert mage.name == 'Mage'
    >>> assert mage.health == 98

    >>> rouge = Hero('Rouge')
    >>> assert rouge.name == 'Rouge'
    >>> assert rouge.health == 76

    >>> cleric = Hero('Cleric')
    >>> assert cleric.name == 'Cleric'
    >>> assert cleric.health == 52
"""
from dataclasses import dataclass, field
from random import randint


# Create class `User`, with attributes:
# - `name: str` (required)
# - `health: int` (optional), default: randint(50, 100)
# Use `dataclass`
# type: type[Hero]
@dataclass
class Hero:
    ...