4.6. Dataclass Mutable Attrs

4.6.1. SetUp

>>> from dataclasses import dataclass, field

4.6.2. Class Problem

  • You should not set mutable objects as a default function argument

  • Problem with dict, list, set

  • This problem occurs also in Java Script, PHP, Ruby, etc

  • This is how all dynamically typed languages work

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 Java Script, PHP, Ruby, etc).

>>> class User:
...     def __init__(self, firstname, lastname, groups=[]):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.groups = groups
>>> mark = User('Mark', 'Watney')
>>> mark.groups.append('user')
>>> mark.groups.append('staff')
>>> mark.groups.append('admin')
>>>
>>> print(mark.groups)
['user', 'staff', 'admin']
>>> melissa = User('Melissa', 'Lewis')
>>>
>>> print(melissa.groups)
['user', 'staff', 'admin']

4.6.3. Class Solution

  • Instead of using list as a default value, use None

>>> class User:
...     def __init__(self, firstname, lastname, groups=None):
...         self.firstname = firstname
...         self.lastname = lastname=
...         self.groups = groups if groups else []
>>> mark = User('Mark', 'Watney')
>>> mark.groups.append('user')
>>> mark.groups.append('staff')
>>> mark.groups.append('admin')
>>>
>>> print(mark.groups)
['user', 'staff', 'admin']
>>> melissa = User('Melissa', 'Lewis')
>>>
>>> print(melissa.groups)
[]

4.6.4. Dataclass Problem

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[str] = []
...
Traceback (most recent call last):
ValueError: mutable default <class 'list'> for field groups is not allowed: use default_factory

4.6.5. Dataclass Solution

>>> @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.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. Default Factory

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

Use Case

@dataclass class Hero:

name: str health: int = randint(50, 100)

mark = Hero('Mark') melissa = Hero('Melissa') rick = Hero('Rick') mark.health 90 melissa.health 90 rick.health 90

@dataclass class Hero:

name: str health: int = field(default_factory=lambda: randint(50, 100))

4.6.8. Assignments

"""
* 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:
    ...


"""
* 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', 'staff']
    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', 'staff']
    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', 'staff']

    >>> 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', 'staff']
# type: type[User]
@dataclass
class User:
    ...


"""
* 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:
    ...