5.9. OOP Object Relations

5.9.1. Rationale

  • pickle - has relations

  • json - has relations

  • csv - non-relational format

5.9.2. Base

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>>
>>> CREW = [
...     Astronaut('Mark', 'Watney'),
...     Astronaut('Melissa', 'Lewis'),
...     Astronaut('Rick', 'Martinez')]
../_images/oop-relations-base.png

5.9.3. Extend

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     role: str
>>>
>>>
>>> CREW = [
...     Astronaut('Mark', 'Watney', 'Botanist'),
...     Astronaut('Melissa', 'Lewis', 'Commander'),
...     Astronaut('Rick', 'Martinez', 'Pilot')]
../_images/oop-relations-extend1.png
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     role: str
...     mission_year: int
...     missions_name: str
>>>
>>>
>>> CREW = [
...     Astronaut('Mark', 'Watney', 'Botanist', 2035, 'Ares 3'),
...     Astronaut('Melissa', 'Lewis', 'Commander', 2035, 'Ares 3'),
...     Astronaut('Rick', 'Martinez', 'Pilot', 2035, 'Ares 3')]
../_images/oop-relations-extend2.png

5.9.4. Boolean Vector

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Mission:
...     year: int
...     name: str
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     role: str
...     missions: list[Mission]
>>>
>>>
>>> CREW = [
...     Astronaut('Mark', 'Watney', 'Botanist', missions=[
...         Mission(2035, 'Ares 3')]),
...     Astronaut('Melissa', 'Lewis', 'Commander', missions=[
...         Mission(2035, 'Ares 3'),
...         Mission(2031, 'Ares 1')]),
...     Astronaut('Rick', 'Martinez', 'Pilot', missions=[])]
../_images/oop-relations-boolvector.png

5.9.5. FFill

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Mission:
...     year: int
...     name: str
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     role: str
...     missions: list[Mission]
>>>
>>>
>>> CREW = [
...     Astronaut('Mark', 'Watney', 'Botanist', missions=[
...         Mission(2035, 'Ares 3')]),
...     Astronaut('Melissa', 'Lewis', 'Commander', missions=[
...         Mission(2035, 'Ares 3'),
...         Mission(2031, 'Ares 1')]),
...     Astronaut('Rick', 'Martinez', 'Pilot', missions=[])]
../_images/oop-relations-ffill-empty.png
../_images/oop-relations-ffill-dash.png
../_images/oop-relations-ffill-duplicate.png
../_images/oop-relations-ffill-uniqid.png

5.9.6. Relations

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Mission:
...     year: int
...     name: str
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     role: str
...     missions: list[Mission]
>>>
>>>
>>> CREW = [
...     Astronaut('Mark', 'Watney', 'Botanist', missions=[
...         Mission(2035, 'Ares 3')]),
...     Astronaut('Melissa', 'Lewis', 'Commander', missions=[
...         Mission(2035, 'Ares 3'),
...         Mission(2031, 'Ares 1')]),
...     Astronaut('Rick', 'Martinez', 'Pilot', missions=[])]
../_images/oop-relations-rel-m2o.png
../_images/oop-relations-rel-m2m.png

5.9.7. Serialization

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Mission:
...     year: int
...     name: str
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     role: str
...     missions: list[Mission]
>>>
>>>
>>> CREW = [
...     Astronaut('Mark', 'Watney', 'Botanist', missions=[
...         Mission(2035, 'Ares 3')]),
...     Astronaut('Melissa', 'Lewis', 'Commander', missions=[
...         Mission(2035, 'Ares 3'),
...         Mission(2031, 'Ares 1')]),
...     Astronaut('Rick', 'Martinez', 'Pilot', missions=[])]
../_images/oop-relations-serialize-cls.png
../_images/oop-relations-serialize-obj.png
../_images/oop-relations-serialize-objattr.png
../_images/oop-relations-serialize-clsattr.png

5.9.8. Assignments

Code 5.8. Solution
"""
* Assignment: OOP Relations Syntax
* Complexity: easy
* Lines of code: 7 lines
* Time: 5 min

English:
    1. Use Dataclass to define class `Point` with attributes:
        a. `x: int` with default value `0`
        b. `y: int` with default value `0`
    2. Use Dataclass to define class `Path` with attributes:
        a. `points: list[Point]` with default empty list
    3. Run doctests - all must succeed

Polish:
    1. Użyj Dataclass do zdefiniowania klasy `Point` z atrybutami:
        a. `x: int` z domyślną wartością `0`
        b. `y: int` z domyślną wartością `0`
    2. Użyj Dataclass do zdefiniowania klasy `Path` z atrybutami:
        a. `points: list[Point]` z domyślną pustą listą
    3. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(Point)
    >>> assert isclass(Path)
    >>> assert hasattr(Point, 'x')
    >>> assert hasattr(Point, 'y')

    >>> Point()
    Point(x=0, y=0)
    >>> Point(x=0, y=0)
    Point(x=0, y=0)
    >>> Point(x=1, y=2)
    Point(x=1, y=2)

    >>> Path([Point(x=0, y=0),
    ...       Point(x=0, y=1),
    ...       Point(x=1, y=0)])
    Path(points=[Point(x=0, y=0), Point(x=0, y=1), Point(x=1, y=0)])
"""

from dataclasses import dataclass, field


Code 5.9. Solution
"""

* Assignment: OOP Relations Model
* Complexity: easy
* Lines of code: 10 lines
* Time: 8 min

English:
    1. In `DATA` we have two classes
    2. Model data using classes and relations
    3. Create instances dynamically based on `DATA`
    4. Run doctests - all must succeed

Polish:
    1. W `DATA` mamy dwie klasy
    2. Zamodeluj problem wykorzystując klasy i relacje między nimi
    3. Twórz instancje dynamicznie na podstawie `DATA`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> assert type(result) is list

    >>> assert all(type(astro) is Astronaut
    ...            for astro in result)

    >>> assert all(type(addr) is Address
    ...            for astro in result
    ...            for addr in astro.addresses)

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [Astronaut(firstname='Jan',
               lastname='Twardowski',
               addresses=[Address(street='Kamienica Pod św. Janem Kapistranem', city='Kraków', postcode='31-008', region='Małopolskie', country='Poland')]),
     Astronaut(firstname='José',
               lastname='Jiménez',
               addresses=[Address(street='2101 E NASA Pkwy', city='Houston', postcode=77058, region='Texas', country='USA'),
                          Address(street='', city='Kennedy Space Center', postcode=32899, region='Florida', country='USA')]),
     Astronaut(firstname='Mark',
               lastname='Watney',
               addresses=[Address(street='4800 Oak Grove Dr', city='Pasadena', postcode=91109, region='California', country='USA'),
                          Address(street='2825 E Ave P', city='Palmdale', postcode=93550, region='California', country='USA')]),
     Astronaut(firstname='Иван',
               lastname='Иванович',
               addresses=[Address(street='', city='Космодро́м Байкону́р', postcode='', region='Кызылординская область', country='Қазақстан'),
                          Address(street='', city='Звёздный городо́к', postcode=141160, region='Московская область', country='Россия')]),
     Astronaut(firstname='Melissa',
               lastname='Lewis',
               addresses=[]),
     Astronaut(firstname='Alex',
               lastname='Vogel',
               addresses=[Address(street='Linder Hoehe', city='Köln', postcode=51147, region='North Rhine-Westphalia', country='Germany')])]
"""

from dataclasses import dataclass
from typing import Optional, Union


DATA = [
    {"firstname": "Jan", "lastname": "Twardowski", "addresses": [
        {"street": "Kamienica Pod św. Janem Kapistranem", "city": "Kraków", "postcode": "31-008", "region": "Małopolskie", "country": "Poland"}]},
    {"firstname": "José", "lastname": "Jiménez", "addresses": [
        {"street": "2101 E NASA Pkwy", "city": "Houston", "postcode": 77058, "region": "Texas", "country": "USA"},
        {"street": "", "city": "Kennedy Space Center", "postcode": 32899, "region": "Florida", "country": "USA"}]},
    {"firstname": "Mark", "lastname": "Watney", "addresses": [
        {"street": "4800 Oak Grove Dr", "city": "Pasadena", "postcode": 91109, "region": "California", "country": "USA"},
        {"street": "2825 E Ave P", "city": "Palmdale", "postcode": 93550, "region": "California", "country": "USA"}]},
    {"firstname": "Иван", "lastname": "Иванович", "addresses": [
        {"street": "", "city": "Космодро́м Байкону́р", "postcode": "", "region": "Кызылординская область", "country": "Қазақстан"},
        {"street": "", "city": "Звёздный городо́к", "postcode": 141160, "region": "Московская область", "country": "Россия"}]},
    {"firstname": "Melissa", "lastname": "Lewis", "addresses": []},
    {"firstname": "Alex", "lastname": "Vogel", "addresses": [
        {"street": "Linder Hoehe", "city": "Köln", "postcode": 51147, "region": "North Rhine-Westphalia", "country": "Germany"}]}
]

result: list


Code 5.10. Solution
"""
* Assignment: OOP Relations Movable
* Complexity: medium
* Lines of code: 18 lines
* Time: 8 min

English:
    1. Define class `Point`
    2. Class `Point` has attributes `x: int = 0` and `y: int = 0`
    3. Define class `Movable`
    4. In `Movable` define method `get_position(self) -> Point`
    5. In `Movable` define method `set_position(self, x: int, y: int) -> None`
    6. In `Movable` define method `change_position(self, left: int = 0, right: int = 0, up: int = 0, down: int = 0) -> None`
    7. Assume left-top screen corner as a initial coordinates position:
        a. going right add to `x`
        b. going left subtract from `x`
        c. going up subtract from `y`
        d. going down add to `y`
    8. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Point`
    2. Klasa `Point` ma atrybuty `x: int = 0` oraz `y: int = 0`
    3. Zdefiniuj klasę `Movable`
    4. W `Movable` zdefiniuj metodę `get_position(self) -> Point`
    5. W `Movable` zdefiniuj metodę `set_position(self, x: int, y: int) -> None`
    6. W `Movable` zdefiniuj metodę `change_position(self, left: int = 0, right: int = 0, up: int = 0, down: int = 0) -> None`
    7. Przyjmij górny lewy róg ekranu za punkt początkowy:
        a. idąc w prawo dodajesz `x`
        b. idąc w lewo odejmujesz `x`
        c. idąc w górę odejmujesz `y`
        d. idąc w dół dodajesz `y`
    8. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(Point)
    >>> assert isclass(Movable)
    >>> assert hasattr(Point, 'x')
    >>> assert hasattr(Point, 'y')
    >>> assert hasattr(Movable, 'get_position')
    >>> assert hasattr(Movable, 'set_position')
    >>> assert hasattr(Movable, 'change_position')
    >>> assert ismethod(Movable().get_position)
    >>> assert ismethod(Movable().set_position)
    >>> assert ismethod(Movable().change_position)

    >>> class Astronaut(Movable):
    ...     pass

    >>> astro = Astronaut()

    >>> astro.set_position(x=1, y=2)
    >>> astro.get_position()
    Point(x=1, y=2)

    >>> astro.set_position(x=1, y=1)
    >>> astro.change_position(right=1)
    >>> astro.get_position()
    Point(x=2, y=1)

    >>> astro.set_position(x=1, y=1)
    >>> astro.change_position(left=1)
    >>> astro.get_position()
    Point(x=0, y=1)

    >>> astro.set_position(x=1, y=1)
    >>> astro.change_position(down=1)
    >>> astro.get_position()
    Point(x=1, y=2)

    >>> astro.set_position(x=1, y=1)
    >>> astro.change_position(up=1)
    >>> astro.get_position()
    Point(x=1, y=0)
"""

from dataclasses import dataclass


Code 5.11. Solution
"""
* Assignment: OOP Relations Flatten
* Complexity: medium
* Lines of code: 5 lines
* Time: 13 min

English:
    1. How to write relations to CSV file (contact has many addresses)?
    2. Convert `DATA` to `resul: list[dict[str,str]]`
    3. Non-functional requirements:
        a. Use `,` to separate fields
        b. Use `;` to separate columns
    4. Run doctests - all must succeed

Polish:
    1. Jak zapisać w CSV dane relacyjne (kontakt ma wiele adresów)?
    2. Przekonwertuj `DATA` do `resul: list[dict[str,str]]`
    3. Wymagania niefunkcjonalne:
        b. Użyj `,` do oddzielenia pól
        b. Użyj `;` do oddzielenia kolumn
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [{'firstname': 'Jan', 'lastname': 'Twardowski', 'missions': '1967,Apollo 1;1970,Apollo 13;1973,Apollo 18'},
     {'firstname': 'Ivan', 'lastname': 'Ivanovic', 'missions': '2023,Artemis 2;2024,Artemis 3'},
     {'firstname': 'Mark', 'lastname': 'Watney', 'missions': '2035,Ares 3'},
     {'firstname': 'Melissa', 'lastname': 'Lewis', 'missions': ''}]
"""

class Astronaut:
    def __init__(self, firstname, lastname, missions=()):
        self.firstname = firstname
        self.lastname = lastname
        self.missions = list(missions)


class Mission:
    def __init__(self, year, name):
        self.year = year
        self.name = name


DATA = [
    Astronaut('Jan', 'Twardowski', missions=[
        Mission('1967', 'Apollo 1'),
        Mission('1970', 'Apollo 13'),
        Mission('1973', 'Apollo 18')]),

    Astronaut('Ivan', 'Ivanovic', missions=[
        Mission('2023', 'Artemis 2'),
        Mission('2024', 'Artemis 3')]),

    Astronaut('Mark', 'Watney', missions=[
        Mission('2035', 'Ares 3')]),

    Astronaut('Melissa', 'Lewis')]


result: list


Code 5.12. Solution
"""
* Assignment: OOP Relations Nested
* Complexity: medium
* Lines of code: 7 lines
* Time: 13 min

English:
    1. Convert `DATA` to format with one column per each attrbute
       for example: `street1`, `street2`, `city1`, `city2`, etc.
    2. Run doctests - all must succeed

Polish:
    1. Przekonweruj `DATA` do formatu z jedną kolumną dla każdego atrybutu,
       np. `street1`, `street2`, `city1`, `city2`, itd.
    2. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> type(result)
    <class 'list'>
    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [{'firstname': 'Jan', 'lastname': 'Twardowski', 'street1': 'Kamienica Pod św. Janem Kapistranem', 'city1': 'Kraków', 'post_code1': '31-008', 'region1': 'Małopolskie', 'country1': 'Poland'},
     {'firstname': 'José', 'lastname': 'Jiménez', 'street1': '2101 E NASA Pkwy', 'city1': 'Houston', 'post_code1': 77058, 'region1': 'Texas', 'country1': 'USA', 'street2': '', 'city2': 'Kennedy Space Center', 'post_code2': 32899, 'region2': 'Florida', 'country2': 'USA'},
     {'firstname': 'Mark', 'lastname': 'Watney', 'street1': '4800 Oak Grove Dr', 'city1': 'Pasadena', 'post_code1': 91109, 'region1': 'California', 'country1': 'USA', 'street2': '2825 E Ave P', 'city2': 'Palmdale', 'post_code2': 93550, 'region2': 'California', 'country2': 'USA'},
     {'firstname': 'Иван', 'lastname': 'Иванович', 'street1': '', 'city1': 'Космодро́м Байкону́р', 'post_code1': '', 'region1': 'Кызылординская область', 'country1': 'Қазақстан', 'street2': '', 'city2': 'Звёздный городо́к', 'post_code2': 141160, 'region2': 'Московская область', 'country2': 'Россия'},
     {'firstname': 'Melissa', 'lastname': 'Lewis'},
     {'firstname': 'Alex', 'lastname': 'Vogel', 'street1': 'Linder Hoehe', 'city1': 'Köln', 'post_code1': 51147, 'region1': 'North Rhine-Westphalia', 'country1': 'Germany'}]
"""

import json

DATA = """[
    {"firstname": "Jan", "lastname": "Twardowski", "addresses": [
        {"street": "Kamienica Pod św. Janem Kapistranem", "city": "Kraków", "post_code": "31-008", "region": "Małopolskie", "country": "Poland"}]},

    {"firstname": "José", "lastname": "Jiménez", "addresses": [
        {"street": "2101 E NASA Pkwy", "city": "Houston", "post_code": 77058, "region": "Texas", "country": "USA"},
        {"street": "", "city": "Kennedy Space Center", "post_code": 32899, "region": "Florida", "country": "USA"}]},

    {"firstname": "Mark", "lastname": "Watney", "addresses": [
        {"street": "4800 Oak Grove Dr", "city": "Pasadena", "post_code": 91109, "region": "California", "country": "USA"},
        {"street": "2825 E Ave P", "city": "Palmdale", "post_code": 93550, "region": "California", "country": "USA"}]},

    {"firstname": "Иван", "lastname": "Иванович", "addresses": [
        {"street": "", "city": "Космодро́м Байкону́р", "post_code": "", "region": "Кызылординская область", "country": "Қазақстан"},
        {"street": "", "city": "Звёздный городо́к", "post_code": 141160, "region": "Московская область", "country": "Россия"}]},

    {"firstname": "Melissa", "lastname": "Lewis", "addresses": []},

    {"firstname": "Alex", "lastname": "Vogel", "addresses": [
        {"street": "Linder Hoehe", "city": "Köln", "post_code": 51147, "region": "North Rhine-Westphalia", "country": "Germany"}]}
]"""

result: list