11.10. Functional Map

  • Map (convert) elements in sequence

  • Generator (lazy evaluated)

  • map(callable, *iterables)

  • required callable - Function

  • required iterables - 1 or many sequence or iterator objects

The map() function in Python is a built-in function that applies a given function to each element of an iterable (such as a list, tuple, or set) and returns a new iterable with the results. It takes two arguments: a function and an iterable.

The function is applied to each element in the iterable, and the results are collected into a new iterable. The resulting iterable can be converted to a list, tuple, or set if desired.

Here's an example of using the map() function to square each number in a list:

>>> data = [1, 2, 3, 4, 5]
>>>
>>> def square(n):
...     return n ** 2
>>>
>>> result = map(square, data)
>>> list(result)
[1, 4, 9, 16, 25]

In this example, the square function is applied to each element in the numbers list using the map() function. The resulting iterable contains the squared values of each element in the original list. The list() function is used to convert the iterable to a list.

>>> from inspect import isgeneratorfunction, isgenerator
>>>
>>>
>>> isgeneratorfunction(map)
False
>>>
>>> result = map(float, [1,2,3])
>>> isgenerator(result)
False

11.10.1. Example

>>> result = (float(x) for x in range(0,5))
>>>
>>> list(result)
[0.0, 1.0, 2.0, 3.0, 4.0]
>>> result = map(float, range(0,5))
>>>
>>> list(result)
[0.0, 1.0, 2.0, 3.0, 4.0]

11.10.2. Problem

>>> data = [1, 2, 3]
>>> result = []
>>>
>>> for x in data:
...     result.append(float(x))
>>>
>>> print(result)
[1.0, 2.0, 3.0]

11.10.3. Solution

>>> data = [1, 2, 3]
>>> result = map(float, data)
>>>
>>> list(result)
[1.0, 2.0, 3.0]

11.10.4. Lazy Evaluation

>>> data = [1, 2, 3]
>>> result = map(float, data)
>>>
>>> next(result)
1.0
>>> next(result)
2.0
>>> next(result)
3.0
>>> next(result)
Traceback (most recent call last):
StopIteration

11.10.5. Multi Parameters

>>> def myfunc(x):
...     return sum(x)
>>>
>>>
>>> DATA = [
...     (1,2),
...     (3,4),
... ]
>>>
>>> result = map(myfunc, DATA)
>>> print(list(result))
[3, 7]

11.10.6. Starmap

>>> from itertools import starmap
>>>
>>>
>>> DATA = [
...     (3.1415, 3),
...     (2.71828, 2)]
>>>
>>> result = starmap(round, DATA)  # round(number=3.1415, ndigits=2)
>>> print(list(result))
[3.142, 2.72]

11.10.7. Partial

>>> from functools import partial
>>>
>>>
>>> myround = partial(round, ndigits=1)
>>> DATA = [1.111, 2.222, 3.333]
>>>
>>> result = map(myround, DATA)  # round(number=1.111, ndigits=1)
>>> print(list(result))
[1.1, 2.2, 3.3]

11.10.8. Performance

>>> def even(x):
...     return x % 2 == 0
>>> 
... %%timeit -r 1000 -n 1000
... result = [float(x) for x in data if even(x)]
1.9 µs ± 206 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)
>>> 
... %%timeit -r 1000 -n 1000
... result = list(map(float, filter(parzysta, data)))
1.66 µs ± 175 ns per loop (mean ± std. dev. of 1000 runs, 1,000 loops each)

11.10.9. Use Case - 0x01

Built-in functions:

>>> DATA = [1, 2, 3]
>>> result = map(float, DATA)
>>>
>>> tuple(map(float, DATA))
(1.0, 2.0, 3.0)
>>> DATA = [1, 2, 3]
>>> result = map(float, DATA)
>>>
>>> set(map(float, DATA))
{1.0, 2.0, 3.0}
>>> DATA = [1, 2, 3]
>>> result = (float(x) for x in DATA)
>>>
>>> list(result)
[1.0, 2.0, 3.0]
>>> DATA = [1.1, 2.2, 3.3]
>>> result = map(round, DATA)
>>>
>>> list(result)
[1, 2, 3]

11.10.10. Use Case - 0x02

>>> def square(x):
...     return x ** 2
>>>
>>>
>>> DATA = [1, 2, 3]
>>> result = map(square, DATA)
>>>
>>> list(result)
[1, 4, 9]

11.10.11. Use Case - 0x03

>>> def increment(x):
...     return x + 1
>>>
>>>
>>> DATA = [1, 2, 3, 4]
>>> result = map(increment, DATA)
>>>
>>> list(result)
[2, 3, 4, 5]

11.10.12. Use Case - 0x04

>>> def translate(letter):
...     return PL.get(letter, letter)
>>>
>>>
>>> DATA = 'zażółć gęślą jaźń'
>>> PL = {'ą': 'a', 'ć': 'c', 'ę': 'e',
...       'ł': 'l', 'ń': 'n', 'ó': 'o',
...       'ś': 's', 'ż': 'z', 'ź': 'z'}
>>>
>>> result = map(translate, DATA)
>>> ''.join(result)
'zazolc gesla jazn'

11.10.13. Use Case - 0x05

Standard input:

>>> import sys
>>>
>>> 
... print(sum(map(int, sys.stdin)))
$ cat ~/.profile |grep addnum
alias addnum='python -c"import sys; print(sum(map(int, sys.stdin)))"'

11.10.14. Use Case - 0x06

>>> import httpx
>>>
>>> url = 'https://python3.info/_static/iris-dirty.csv'
>>>
>>> data = httpx.get(url).text
>>> header, *rows = data.splitlines()
>>> nrows, nfeatures, *class_labels = header.strip().split(',')
>>> label_encoder = dict(enumerate(class_labels))
>>> result = []
>>> for row in rows:
...     *features, species = row.strip().split(',')
...     features = map(float, features)
...     species = label_encoder[int(species)]
...     row = tuple(features) + (species,)
...     result.append(row)
>>> def decode(row):
...     *features, species = row.strip().split(',')
...     features = map(float, features)
...     species = label_encoder[int(species)]
...     return tuple(features) + (species,)
>>>
>>> result = map(decode, rows)
>>> def decode(row):
...     *features, species = row.strip().split(',')
...     features = map(float, features)
...     species = label_encoder[int(species)]
...     return tuple(features) + (species,)
>>>
>>> with open('/tmp/myfile.csv') as file:  
...     header = file.readline()
...     for line in map(decode, file):
...         print(line)

11.10.15. Use Case - 0x07

SetUp:

>>> from doctest import testmod as run_tests

Data [1]:

>>> DATA = """150,4,setosa,versicolor,virginica
... 5.1,3.5,1.4,0.2,0
... 7.0,3.2,4.7,1.4,1
... 6.3,3.3,6.0,2.5,2
... 4.9,3.0,1.4,0.2,0
... 6.4,3.2,4.5,1.5,1
... 5.8,2.7,5.1,1.9,2"""

Definition:

>>> def get_labelencoder(header: str) -> dict[int, str]:
...     """
...     >>> get_labelencoder('150,4,setosa,versicolor,virginica')
...     {0: 'setosa', 1: 'versicolor', 2: 'virginica'}
...     """
...     nrows, nfeatures, *class_labels = header.split(',')
...     return dict(enumerate(class_labels))
>>>
>>> run_tests()  
TestResults(failed=0, attempted=1)
>>> def get_data(line: str) -> tuple:
...     """
...     >>> convert('5.1,3.5,1.4,0.2,0')
...     (5.1, 3.5, 1.4, 0.2, 'setosa')
...     >>> convert('7.0,3.2,4.7,1.4,1')
...     (7.0, 3.2, 4.7, 1.4, 'versicolor')
...     >>> convert('6.3,3.3,6.0,2.5,2')
...     (6.3, 3.3, 6.0, 2.5, 'virginica')
...     """
...     *values, species = line.split(',')
...     values = map(float, values)
...     species = label_encoder[int(species)]
...     return tuple(values) + (species,)
>>>
>>> run_tests()  
TestResults(failed=0, attempted=3)
>>> header, *lines = DATA.splitlines()
>>> label_encoder = get_labelencoder(header)
>>> result = map(get_data, lines)
>>> list(result)  
[(5.1, 3.5, 1.4, 0.2, 'setosa'),
 (7.0, 3.2, 4.7, 1.4, 'versicolor'),
 (6.3, 3.3, 6.0, 2.5, 'virginica'),
 (4.9, 3.0, 1.4, 0.2, 'setosa'),
 (6.4, 3.2, 4.5, 1.5, 'versicolor'),
 (5.8, 2.7, 5.1, 1.9, 'virginica')]

11.10.16. Use Case - 0x08

>>> 
... import pandas as pd
...
...
... DATA = 'https://python3.info/_static/phones-pl.csv'
...
... result = (
...     pd
...     .read_csv(DATA, parse_dates=['datetime'])
...     .set_index('datetime', drop=True)
...     .drop(columns=['id'])
...     .loc['2000-01-01':'2000-03-01']
...     .query('item == "sms"')
...     .groupby(['period','item'])
...     .agg(
...         duration_count = ('duration', 'count'),
...         duration_sum = ('duration', 'sum'),
...         duration_median = ('duration', 'median'),
...         duration_mean = ('duration', 'mean'),
...         duration_std = ('duration', 'std'),
...         duration_var = ('duration', 'var'),
...         value = ('duration', lambda column: column.mean().astype(int))
...     )
... )

11.10.17. References

11.10.18. Assignments

"""
* Assignment: Functional Apply Map
* Type: class assignment
* Complexity: easy
* Lines of code: 3 lines
* Time: 3 min

English:
    1. Define function `cube()`:
       a. takes one argument
       b. returns its argument cubed (raised to the power of 3)
    2. Use `map()` to apply function `cube()` to DATA
    3. Define `result: map` with result
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj funckję `cube()`:
       a. przyjmuje jeden argument
       b. zwraca argument podniesiony do sześcianu (do 3 potęgi)
    2. Użyj `map()` aby zaaplikować funkcję `cube()` do DATA
    3. Zdefiniuj `result: map` z wynikiem
    4. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isfunction(cube), \
    'Object `cube` must be a function'
    >>> assert result is not Ellipsis, \
    'Assign result to variable: `result`'

    >>> assert type(result) is map, \
    'Variable `result` has invalid type, should be map'

    >>> result = list(result)
    >>> assert type(result) is list, \
    'Evaluated `result` has invalid type, should be list'

    >>> assert all(type(x) is int for x in result), \
    'All rows in `result` should be int'

    >>> result
    [0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
"""


DATA = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


# Returns its argument cubed (raised to the power of 3)
# type: Callable[[int], int]
def cube(x):
    ...

# Cube numbers in DATA
# type: map
result = ...


"""
* Assignment: Functional About FromISOFormat
* Complexity: easy
* Lines of code: 1 lines
* Time: 3 min

English:
    1. Define `result: map` with parsed `DATA` dates
    2. Use `map()` and `datetime.fromisoformat()`
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj `result: map` ze sparsowanymi datami `DATA`
    2. Użyj `map()` oraz `datetime.fromisoformat()`
    3. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `map()`
    * `datetime.fromisoformat()`

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

    >>> assert result is not Ellipsis, \
    'Assign result to variable: `result`'
    >>> assert type(result) is map, \
    'Variable `result` has invalid type, must be a map'

    >>> result = list(result)
    >>> assert type(result) is list, \
    'Variable `result` has invalid type, must be a list'

    >>> assert all(type(element) is datetime for element in result), \
    'All elements in `result` must be a datetime'

    >>> pprint(result, width=30)
    [datetime.datetime(1961, 4, 12, 6, 7),
     datetime.datetime(1961, 4, 12, 6, 7)]
"""

from datetime import datetime

DATA = [
    '1961-04-12 06:07',
    '1961-04-12 06:07:00',
]

# Define `result: map` with parsed `DATA` dates
# type: map
result = ...

"""
* Assignment: Functional About DateFormats
* Complexity: medium
* Lines of code: 7 lines
* Time: 5 min

English:
    1. Define `result: map` with parsed `DATA` dates
    2. Use `map()`
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj `result: map` ze sparsowanymi datami `DATA`
    2. Użyj `map()`
    3. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `for ... in`
    * nested `try ... except`
    * FORMATS = []
    * for fmt in FORMATS
    * helper function
    * 24-hour clock
    * map(func, iterable1, iterable1)

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

    >>> assert result is not Ellipsis, \
    'Assign result to variable: `result`'
    >>> assert type(result) is map, \
    'Variable `result` has invalid type, must be a map'
    >>> result = list(result)
    >>> assert type(result) is list, \
    'Variable `result` has invalid type, must be a list'
    >>> assert all(type(element) is datetime for element in result), \
    'All elements in `result` must be a datetime'

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [datetime.datetime(1957, 10, 4, 19, 28, 34),
     datetime.datetime(1961, 4, 12, 6, 7),
     datetime.datetime(1969, 7, 21, 2, 56, 15)]
"""

from datetime import datetime


DATA = [
    'Oct 4, 1957 19:28:34',  # Sputnik launch (first satellite in space)
    'April 12, 1961 6:07',  # Gagarin launch (first human in space)
    'July 21, 1969 2:56:15',  # Armstrong first step on the Moon
]

FORMATS = [
    '%b %d, %Y %H:%M:%S',
    '%B %d, %Y %H:%M',
    '%B %d, %Y %H:%M:%S',
]


# DATA elements in datetime format
# type: map
result = ...


"""
* Assignment: Functional About Log
* Complexity: medium
* Lines of code: 7 lines
* Time: 8 min

English:
    1. Iterate over `DATA` with Apollo 11 timeline [1]
    2. From each line extract date, time, level and message
    3. Collect data to `result: map`
    4. Run doctests - all must succeed

Polish:
    1. Iteruj po `DATA` z harmonogramem Apollo 11 [1]
    2. Dla każdej linii wyciągnij datę, czas, poziom logowania oraz wiadomość
    3. Zbierz dane do `result: map`
    4. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * Note, that last time has no seconds
    * This is not bug, time without seconds is in NASA history records [1]

References:
    [1] National Aeronautics and Space Administration.
        Apollo 11 timeline.
        Year: 1969. Retrieved: 2021-03-25.
        URL: https://history.nasa.gov/SP-4029/Apollo_11i_Timeline.htm

Hints:
    * `str.splitlines()`
    * `str.split(', ', maxsplit=3)`
    * `date.fromisoformat()`
    * `time.fromisoformat()`
    * `datetime.combine()`

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

    >>> assert result is not Ellipsis, \
    'Assign result to variable: `result`'
    >>> assert type(result) is map, \
    'Variable `result` has invalid type, must be a map'

    >>> result = list(result)
    >>> assert all(type(row) is dict for row in result), \
    'All elements in result must be dict'

    >>> pprint(result)
    [{'datetime': datetime.datetime(1969, 7, 14, 21, 0),
      'level': 'INFO',
      'message': 'Terminal countdown started'},
     {'datetime': datetime.datetime(1969, 7, 16, 13, 31, 53),
      'level': 'WARNING',
      'message': 'S-IC engine ignition (#5)'},
     {'datetime': datetime.datetime(1969, 7, 16, 13, 33, 23),
      'level': 'DEBUG',
      'message': 'Maximum dynamic pressure (735.17 lb/ft^2)'},
     {'datetime': datetime.datetime(1969, 7, 16, 13, 34, 44),
      'level': 'WARNING',
      'message': 'S-II ignition'},
     {'datetime': datetime.datetime(1969, 7, 16, 13, 35, 17),
      'level': 'DEBUG',
      'message': 'Launch escape tower jettisoned'},
     {'datetime': datetime.datetime(1969, 7, 16, 13, 39, 40),
      'level': 'DEBUG',
      'message': 'S-II center engine cutoff'},
     {'datetime': datetime.datetime(1969, 7, 16, 16, 22, 13),
      'level': 'INFO',
      'message': 'Translunar injection'},
     {'datetime': datetime.datetime(1969, 7, 16, 16, 56, 3),
      'level': 'INFO',
      'message': 'CSM docked with LM/S-IVB'},
     {'datetime': datetime.datetime(1969, 7, 16, 17, 21, 50),
      'level': 'INFO',
      'message': 'Lunar orbit insertion ignition'},
     {'datetime': datetime.datetime(1969, 7, 16, 21, 43, 36),
      'level': 'INFO',
      'message': 'Lunar orbit circularization ignition'},
     {'datetime': datetime.datetime(1969, 7, 20, 17, 44),
      'level': 'INFO',
      'message': 'CSM/LM undocked'},
     {'datetime': datetime.datetime(1969, 7, 20, 20, 5, 5),
      'level': 'WARNING',
      'message': 'LM powered descent engine ignition'},
     {'datetime': datetime.datetime(1969, 7, 20, 20, 10, 22),
      'level': 'ERROR',
      'message': 'LM 1202 alarm'},
     {'datetime': datetime.datetime(1969, 7, 20, 20, 14, 18),
      'level': 'ERROR',
      'message': 'LM 1201 alarm'},
     {'datetime': datetime.datetime(1969, 7, 20, 20, 17, 39),
      'level': 'WARNING',
      'message': 'LM lunar landing'},
     {'datetime': datetime.datetime(1969, 7, 21, 2, 39, 33),
      'level': 'DEBUG',
      'message': 'EVA started (hatch open)'},
     {'datetime': datetime.datetime(1969, 7, 21, 2, 56, 15),
      'level': 'WARNING',
      'message': '1st step taken lunar surface (CDR)'},
     {'datetime': datetime.datetime(1969, 7, 21, 2, 56, 15),
      'level': 'WARNING',
      'message': 'Neil Armstrong first words on the Moon'},
     {'datetime': datetime.datetime(1969, 7, 21, 3, 5, 58),
      'level': 'DEBUG',
      'message': 'Contingency sample collection started (CDR)'},
     {'datetime': datetime.datetime(1969, 7, 21, 3, 15, 16),
      'level': 'INFO',
      'message': 'LMP on lunar surface'},
     {'datetime': datetime.datetime(1969, 7, 21, 5, 11, 13),
      'level': 'DEBUG',
      'message': 'EVA ended (hatch closed)'},
     {'datetime': datetime.datetime(1969, 7, 21, 17, 54),
      'level': 'WARNING',
      'message': 'LM lunar liftoff ignition (LM APS)'},
     {'datetime': datetime.datetime(1969, 7, 21, 21, 35),
      'level': 'INFO',
      'message': 'CSM/LM docked'},
     {'datetime': datetime.datetime(1969, 7, 22, 4, 55, 42),
      'level': 'WARNING',
      'message': 'Transearth injection ignition (SPS)'},
     {'datetime': datetime.datetime(1969, 7, 24, 16, 21, 12),
      'level': 'INFO',
      'message': 'CM/SM separation'},
     {'datetime': datetime.datetime(1969, 7, 24, 16, 35, 5),
      'level': 'WARNING',
      'message': 'Entry'},
     {'datetime': datetime.datetime(1969, 7, 24, 16, 50, 35),
      'level': 'WARNING',
      'message': 'Splashdown (went to apex-down)'},
     {'datetime': datetime.datetime(1969, 7, 24, 17, 29),
      'level': 'INFO',
      'message': 'Crew egress'}]
"""
from datetime import date, datetime, time


DATA = """1969-07-14, 21:00:00, INFO, Terminal countdown started
1969-07-16, 13:31:53, WARNING, S-IC engine ignition (#5)
1969-07-16, 13:33:23, DEBUG, Maximum dynamic pressure (735.17 lb/ft^2)
1969-07-16, 13:34:44, WARNING, S-II ignition
1969-07-16, 13:35:17, DEBUG, Launch escape tower jettisoned
1969-07-16, 13:39:40, DEBUG, S-II center engine cutoff
1969-07-16, 16:22:13, INFO, Translunar injection
1969-07-16, 16:56:03, INFO, CSM docked with LM/S-IVB
1969-07-16, 17:21:50, INFO, Lunar orbit insertion ignition
1969-07-16, 21:43:36, INFO, Lunar orbit circularization ignition
1969-07-20, 17:44:00, INFO, CSM/LM undocked
1969-07-20, 20:05:05, WARNING, LM powered descent engine ignition
1969-07-20, 20:10:22, ERROR, LM 1202 alarm
1969-07-20, 20:14:18, ERROR, LM 1201 alarm
1969-07-20, 20:17:39, WARNING, LM lunar landing
1969-07-21, 02:39:33, DEBUG, EVA started (hatch open)
1969-07-21, 02:56:15, WARNING, 1st step taken lunar surface (CDR)
1969-07-21, 02:56:15, WARNING, Neil Armstrong first words on the Moon
1969-07-21, 03:05:58, DEBUG, Contingency sample collection started (CDR)
1969-07-21, 03:15:16, INFO, LMP on lunar surface
1969-07-21, 05:11:13, DEBUG, EVA ended (hatch closed)
1969-07-21, 17:54:00, WARNING, LM lunar liftoff ignition (LM APS)
1969-07-21, 21:35:00, INFO, CSM/LM docked
1969-07-22, 04:55:42, WARNING, Transearth injection ignition (SPS)
1969-07-24, 16:21:12, INFO, CM/SM separation
1969-07-24, 16:35:05, WARNING, Entry
1969-07-24, 16:50:35, WARNING, Splashdown (went to apex-down)
1969-07-24, 17:29, INFO, Crew egress"""

# representation of DATA; dict keys: datetime, level, message
# type: map
result = ...

"""
* Assignment: Functional About CSV
* Complexity: easy
* Lines of code: 5 lines
* Time: 5 min

English:
    1. Convert `DATA` to `result: map`
    2. Convert numeric values to `float`
    3. Run doctests - all must succeed

Polish:
    1. Przekonwertuj `DATA` to `result: map`
    2. Przekonwertuj wartości numeryczne do `float`
    3. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `str.strip()`
    * `str.split()`
    * `map()`
    * `list() + list()`
    * `list.append()`
    * `tuple()`

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

    >>> assert result is not Ellipsis, \
    'Assign result to variable: `result`'
    >>> assert type(result) is map, \
    'Variable `result` has invalid type, must be a map'

    >>> result = list(result)  # expand map object
    >>> assert type(result) is list, \
    'Variable `result` has invalid type, should be list'
    >>> assert all(type(x) is tuple for x in result), \
    'All rows in `result` should be tuple'

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [(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')]
"""

DATA = """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"""

# values from file (note the list[tuple] format!)
# type: map
result = ...

"""
* Assignment: Functional About JSON
* Complexity: medium
* Lines of code: 5 lines
* Time: 8 min

English:
    1. Convert from JSON format to Python using decoder function
    2. Create instances of `Setosa`, `Virginica`, `Versicolor`
       classes based on value in field "species"
    3. Generate instances in `result: map`
    4. Run doctests - all must succeed

Polish:
    1. Przekonwertuj dane z JSON do Python używając dekodera funkcyjnego
    2. Twórz obiekty klas `Setosa`, `Virginica`, `Versicolor`
       w zależności od wartości pola "species"
    3. Generuj instancje w `result: map`
    4. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * `dict.pop()`
    * `globals()[clsname]`
    * `cls(**dict)`
    * `json.loads()`

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

    >>> assert type(result) is map
    >>> result = list(result)
    >>> assert len(result) == 9

    >>> classes = (Setosa, Virginica, Versicolor)
    >>> assert all(type(row) in classes for row in result)

    >>> result[0]
    Virginica(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9)

    >>> result[1]
    Setosa(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2)
"""

import json
from dataclasses import dataclass

FILE = r'_temporary.json'

DATA = (
    '[{"sepal_length":5.8,"sepal_width":2.7,"petal_length":5.1,"petal_widt'
    'h":1.9,"species":"virginica"},{"sepal_length":5.1,"sepal_width":3.5,"'
    'petal_length":1.4,"petal_width":0.2,"species":"setosa"},{"sepal_lengt'
    'h":5.7,"sepal_width":2.8,"petal_length":4.1,"petal_width":1.3,"specie'
    's":"versicolor"},{"sepal_length":6.3,"sepal_width":2.9,"petal_length"'
    ':5.6,"petal_width":1.8,"species":"virginica"},{"sepal_length":6.4,"se'
    'pal_width":3.2,"petal_length":4.5,"petal_width":1.5,"species":"versic'
    'olor"},{"sepal_length":4.7,"sepal_width":3.2,"petal_length":1.3,"peta'
    'l_width":0.2,"species":"setosa"},{"sepal_length":7.0,"sepal_width":3.'
    '2,"petal_length":4.7,"petal_width":1.4,"species":"versicolor"},{"sepa'
    'l_length":7.6,"sepal_width":3.0,"petal_length":6.6,"petal_width":2.1,'
    '"species":"virginica"},{"sepal_length":4.9,"sepal_width":3.0,"petal_l'
    'ength":1.4,"petal_width":0.2,"species":"setosa"}]'
)


@dataclass
class Iris:
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float


class Setosa(Iris):
    pass


class Virginica(Iris):
    pass


class Versicolor(Iris):
    pass



# JSON decoded DATA
# type: map
result = ...


"""
* Assignment: Functional ApplyMap FlattenDicts
* Complexity: medium
* Lines of code: 7 lines
* Time: 13 min

English:
    1. Convert `DATA` to format with one column per each attrbute for example:
       a. `group1_gid`, `group2_gid`,
       b. `group1_name`, `group2_name`
    2. Note, that enumeration starts with one
    3. Collect data to `result: map`
    4. Run doctests - all must succeed

Polish:
    1. Przekonweruj `DATA` do formatu z jedną kolumną dla każdego atrybutu, np:
       a. `group1_gid`, `group2_gid`,
       b. `group1_name`, `group2_name`
    2. Zwróć uwagę, że enumeracja zaczyna się od jeden
    3. Zbierz dane do `result: map`
    4. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> result = list(result)
    >>> assert type(result) is list
    >>> assert len(result) > 0
    >>> assert all(type(x) is dict for x in result)

    >>> pprint(result, width=30, sort_dicts=False)
    [{'firstname': 'Mark',
      'lastname': 'Watney',
      'group1_gid': 1,
      'group1_name': 'staff'},
     {'firstname': 'Melissa',
      'lastname': 'Lewis',
      'group1_gid': 1,
      'group1_name': 'staff',
      'group2_gid': 2,
      'group2_name': 'admins'},
     {'firstname': 'Rick',
      'lastname': 'Martinez'}]
"""

DATA = [
    {"firstname": "Mark", "lastname": "Watney", "groups": [
        {"gid": 1, "name": "staff"}]},

    {"firstname": "Melissa", "lastname": "Lewis", "groups": [
        {"gid": 1, "name": "staff"},
        {"gid": 2, "name": "admins"}]},

    {"firstname": "Rick", "lastname": "Martinez", "groups": []},
]

# Flatten data, each mission field prefixed with mission and number
# type: list[dict]
result = ...


"""
* Assignment: Functional ApplyMap FlattenClasses
* Complexity: medium
* Lines of code: 9 lines
* Time: 13 min

English:
    1. Convert `DATA` to format with one column per each attrbute for example:
       a. `Group1_year`, `Group2_year`,
       b. `Group1_name`, `Group2_name`
    2. Note, that enumeration starts with one
    3. Run doctests - all must succeed

Polish:
    1. Przekonweruj `DATA` do formatu z jedną kolumną dla każdego atrybutu, np:
       a. `Group1_year`, `Group2_year`,
       b. `Group1_name`, `Group2_name`
    2. Zwróć uwagę, że enumeracja zaczyna się od jeden
    3. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * vars(obj) -> dict

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

    >>> result = list(result)
    >>> assert type(result) is list
    >>> assert len(result) > 0
    >>> assert all(type(x) is dict for x in result)

    >>> pprint(result, width=30, sort_dicts=False)
    [{'firstname': 'Mark',
      'lastname': 'Watney',
      'group1_gid': 1,
      'group1_name': 'staff'},
     {'firstname': 'Melissa',
      'lastname': 'Lewis',
      'group1_gid': 1,
      'group1_name': 'staff',
      'group2_gid': 2,
      'group2_name': 'admins'},
     {'firstname': 'Rick',
      'lastname': 'Martinez'}]
"""

class User:
    def __init__(self, firstname, lastname, groups=()):
        self.firstname = firstname
        self.lastname = lastname
        self.groups = list(groups)


class Group:
    def __init__(self, gid, name):
        self.gid = gid
        self.name = name

DATA = [
    User('Mark', 'Watney', groups=[
        Group(gid=1, name='staff')]),

    User('Melissa', 'Lewis', groups=[
        Group(gid=1, name='staff'),
        Group(gid=2, name='admins')]),

    User('Rick', 'Martinez'),
]


# Convert DATA
# type: list[dict]
result = ...


"""
* Assignment: Functional ApplyMap Model
* Complexity: easy
* Lines of code: 16 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
    >>> from pprint import pprint

    >>> result = list(result)
    >>> assert type(result) is list
    >>> assert all(type(user) is User for user in result)

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

    >>> pprint(result)  # doctest: +NORMALIZE_WHITESPACE
    [User(firstname='Mark',
          lastname='Watney',
          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')]),
     User(firstname='Melissa',
          lastname='Lewis',
          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')]),
     User(firstname='Rick', lastname='Martinez', addresses=[]),
     User(firstname='Alex',
          lastname='Vogel',
          addresses=[Address(street='Linder Hoehe',
                             city='Cologne',
                             postcode=51147,
                             region='North Rhine-Westphalia',
                             country='Germany')])]
"""

from dataclasses import dataclass
from itertools import starmap

DATA = [
    {"firstname": "Mark", "lastname": "Watney", "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": "Melissa", "lastname": "Lewis", "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": "Rick", "lastname": "Martinez", "addresses": []},

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


@dataclass
class Address:
    street: str
    city: str
    postcode: int
    region: str
    country: str


@dataclass
class User:
    firstname: str
    lastname: str
    addresses: list[Address | None]


# Iterate over `DATA` and create instances
# type: list[User]
result = ...


"""
* Assignment: Functional ApplyMap CSV
* Complexity: medium
* Lines of code: 4 lines
* Time: 13 min

English:
    1. Write data with relations to CSV format
    2. Convert `DATA` to `result: list[dict[str,str]]`
    3. Non-functional requirements:
        a. Use `,` to separate fields
        b. Use `;` to separate instances
    4. Contact has zero or many addresses
    5. Run doctests - all must succeed

Polish:
    1. Zapisz dane relacyjne do formatu CSV
    2. Przekonwertuj `DATA` do `result: list[dict[str,str]]`
    3. Wymagania niefunkcjonalne:
        b. Użyj `,` do oddzielenia pól
        b. Użyj `;` do oddzielenia instancji
    4. Kontakt ma zero lub wiele adresów
    5. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> result = list(result)
    >>> assert type(result) is list
    >>> assert len(result) > 0
    >>> assert all(type(x) is dict for x in result)

    >>> pprint(result, width=112, sort_dicts=False)  # doctest: +NORMALIZE_WHITESPACE
    [{'firstname': 'Mark',
      'lastname': 'Watney',
      'addresses': '2101 E NASA Pkwy,Houston,77058,Texas,USA;,Kennedy Space Center,32899,Florida,USA'},
     {'firstname': 'Melissa',
      'lastname': 'Lewis',
      'addresses': '4800 Oak Grove Dr,Pasadena,91109,California,USA;2825 E Ave P,Palmdale,93550,California,USA'},
     {'firstname': 'Rick', 'lastname': 'Martinez', 'addresses': ''},
     {'firstname': 'Alex',
      'lastname': 'Vogel',
      'addresses': 'Linder Hoehe,Cologne,51147,North Rhine-Westphalia,Germany'}]

"""


DATA = [
    {"firstname": "Mark", "lastname": "Watney", "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": "Melissa", "lastname": "Lewis", "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": "Rick", "lastname": "Martinez", "addresses": []},

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

# Flatten data, each address field prefixed with address and number
# type: list[dict]
result = ...