2.4. Unpacking Arguments

2.4.1. Recap

  • argument - Value/variable/reference being passed to the function

  • positional argument - Value passed to function - order is important

  • keyword arguments - Value passed to function resolved by name - order is not important

  • keyword arguments must be on the right side

  • order of keyword arguments doesn't matter

>>> echo(1)          # positional argument
>>> echo(a=1)        # keyword argument
>>> echo(1, 2)       # positional arguments
>>> echo(2, 1)       # positional arguments
>>> echo(a=1, b=2)   # keyword arguments
>>> echo(b=2, a=1)   # keyword arguments, order doesn't matter
>>> echo(1, b=2)     # positional and keyword arguments
>>> echo(a=1, 2)
Traceback (most recent call last):
SyntaxError: positional argument follows keyword argument

2.4.2. Rationale

  • Unpacking and Arbitrary Number of Parameters and Arguments

../../_images/unpacking-assignment,args,params.png

2.4.3. Positional Arguments

  • * is used for positional arguments

  • there is no convention, but you can use any name

  • * unpacks from tuple, list or set

>>> def echo(a, b, c=0):
...     print(f'{a=}, {b=}, {c=}')
>>>
>>>
>>> echo(1, 2)
a=1, b=2, c=0
>>>
>>> data = (1, 2)
>>> echo(data)
Traceback (most recent call last):
TypeError: echo() missing 1 required positional argument: 'b'
>>>
>>> data = (1, 2)
>>> echo(data[0], data[1])
a=1, b=2, c=0
>>>
>>> data = (1, 2)
>>> echo(*data)
a=1, b=2, c=0

2.4.4. Keyword Arguments

  • ** is used for keyword arguments

  • there is no convention, but you can use any name

  • ** unpacks from dict

Keyword arguments passed directly:

>>> def echo(a, b, c=0):
...     print(f'{a=}, {b=}, {c=}')
>>>
>>>
>>> echo(a=1, b=2)
a=1, b=2, c=0
>>>
>>> data = {'a': 1, 'b': 2}
>>> echo(a=data['a'], b=data['b'])
a=1, b=2, c=0
>>>
>>> data = {'a': 1, 'b': 2}
>>> echo(**data)
a=1, b=2, c=0

2.4.5. Positional and Keyword Arguments

>>> def echo(a, b, c=0):
...     print(f'{a=}, {b=}, {c=}')
>>>
>>>
>>> echo(1, b=2)
a=1, b=2, c=0
>>>
>>> data1 = (1,)
>>> data2 = {'b': 2}
>>> echo(data1[0], b=data2['b'])
a=1, b=2, c=0
>>>
>>> data1 = (1,)
>>> data2 = {'b': 2}
>>> echo(*data1, **data2)
a=1, b=2, c=0
>>>
>>> data1 = (1, 2)
>>> data2 = {'b': 2}
>>> echo(*data1, **data2)
Traceback (most recent call last):
TypeError: echo() got multiple values for argument 'b'

2.4.6. Objects From Sequence

>>> class Iris:
...     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
>>>
>>>
>>> DATA = (6.0, 3.4, 4.5, 1.6, 'versicolor')
>>>
>>> result = Iris(*DATA)
>>> vars(result)  
{'sepal_length': 6.0,
 'sepal_width': 3.4,
 'petal_length': 4.5,
 'petal_width': 1.6,
 'species': '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'),
...         (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:
...     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 __repr__(self):
...         return str(vars(self))
>>>
>>>
>>> result = [Iris(*row) for row in DATA]
>>> print(result)  
[{'sepal_length': 5.8, 'sepal_width': 2.7, 'petal_length': 5.1, 'petal_width': 1.9, 'species': 'virginica'},
 {'sepal_length': 5.1, 'sepal_width': 3.5, 'petal_length': 1.4, 'petal_width': 0.2, 'species': 'setosa'},
 {'sepal_length': 5.7, 'sepal_width': 2.8, 'petal_length': 4.1, 'petal_width': 1.3, 'species': 'versicolor'},
 {'sepal_length': 6.3, 'sepal_width': 2.9, 'petal_length': 5.6, 'petal_width': 1.8, 'species': 'virginica'},
 {'sepal_length': 6.4, 'sepal_width': 3.2, 'petal_length': 4.5, 'petal_width': 1.5, 'species': 'versicolor'},
 {'sepal_length': 4.7, 'sepal_width': 3.2, 'petal_length': 1.3, 'petal_width': 0.2, 'species': 'setosa'}]

2.4.7. Objects From Mappings

>>> class Iris:
...     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
>>>
>>>
>>> DATA = {"sepal_length":5.8,"sepal_width":2.7,"petal_length":5.1,"petal_width":1.9,"species":"virginica"}
>>>
>>> iris = Iris(**DATA)
>>> vars(iris)  
{'sepal_length': 5.8,
 'sepal_width': 2.7,
 'petal_length': 5.1,
 'petal_width': 1.9,
 'species': 'virginica'}
>>> class Iris:
...     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 __repr__(self):
...         return str(vars(self))
>>>
>>>
>>> DATA = [{"sepal_length":5.8,"sepal_width":2.7,"petal_length":5.1,"petal_width":1.9,"species":"virginica"},
...         {"sepal_length":5.1,"sepal_width":3.5,"petal_length":1.4,"petal_width":0.2,"species":"setosa"},
...         {"sepal_length":5.7,"sepal_width":2.8,"petal_length":4.1,"petal_width":1.3,"species":"versicolor"},
...         {"sepal_length":6.3,"sepal_width":2.9,"petal_length":5.6,"petal_width":1.8,"species":"virginica"},
...         {"sepal_length":6.4,"sepal_width":3.2,"petal_length":4.5,"petal_width":1.5,"species":"versicolor"},
...         {"sepal_length":4.7,"sepal_width":3.2,"petal_length":1.3,"petal_width":0.2,"species":"setosa"}]
>>>
>>> result = [Iris(**row) for row in DATA]
>>> print(result)  
[{'sepal_length': 5.8, 'sepal_width': 2.7, 'petal_length': 5.1, 'petal_width': 1.9, 'species': 'virginica'},
 {'sepal_length': 5.1, 'sepal_width': 3.5, 'petal_length': 1.4, 'petal_width': 0.2, 'species': 'setosa'},
 {'sepal_length': 5.7, 'sepal_width': 2.8, 'petal_length': 4.1, 'petal_width': 1.3, 'species': 'versicolor'},
 {'sepal_length': 6.3, 'sepal_width': 2.9, 'petal_length': 5.6, 'petal_width': 1.8, 'species': 'virginica'},
 {'sepal_length': 6.4, 'sepal_width': 3.2, 'petal_length': 4.5, 'petal_width': 1.5, 'species': 'versicolor'},
 {'sepal_length': 4.7, 'sepal_width': 3.2, 'petal_length': 1.3, 'petal_width': 0.2, 'species': 'setosa'}]

2.4.8. Use Case - Movement

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Point:
...     x: int
...     y: int
...     z: int = 0
>>>
>>>
>>> MOVEMENT = [(0, 0),
...             (1, 0),
...             (2, 1, 1),
...             (3, 2),
...             (3, 3, -1),
...             (2, 3)]
>>>
>>> movement = [Point(x,y) for x,y in MOVEMENT]
Traceback (most recent call last):
ValueError: too many values to unpack (expected 2)
>>>
>>> movement = [Point(*coordinates) for coordinates in MOVEMENT]
>>> movement  
[Point(x=0, y=0, z=0),
 Point(x=1, y=0, z=0),
 Point(x=2, y=1, z=1),
 Point(x=3, y=2, z=0),
 Point(x=3, y=3, z=-1),
 Point(x=2, y=3, z=0)]

2.4.9. Use Case - Dataclass Args

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Iris:
...     sepal_length: float
...     sepal_width: float
...     petal_length: float
...     petal_width: float
...     species: str
>>>
>>>
>>> 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'),
...         (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')]
>>>
>>>
>>> result = [Iris(*row) for row in DATA]
>>> print(result)  
[Iris(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9, species='virginica'),
 Iris(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2, species='setosa'),
 Iris(sepal_length=5.7, sepal_width=2.8, petal_length=4.1, petal_width=1.3, species='versicolor'),
 Iris(sepal_length=6.3, sepal_width=2.9, petal_length=5.6, petal_width=1.8, species='virginica'),
 Iris(sepal_length=6.4, sepal_width=3.2, petal_length=4.5, petal_width=1.5, species='versicolor'),
 Iris(sepal_length=4.7, sepal_width=3.2, petal_length=1.3, petal_width=0.2, species='setosa')]

2.4.10. Use Case - Dataclass KWArgs

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Iris:
...     sepal_length: float
...     sepal_width: float
...     petal_length: float
...     petal_width: float
...     species: str
>>>
>>>
>>> DATA = [{"sepal_length":5.8,"sepal_width":2.7,"petal_length":5.1,"petal_width":1.9,"species":"virginica"},
...         {"sepal_length":5.1,"sepal_width":3.5,"petal_length":1.4,"petal_width":0.2,"species":"setosa"},
...         {"sepal_length":5.7,"sepal_width":2.8,"petal_length":4.1,"petal_width":1.3,"species":"versicolor"},
...         {"sepal_length":6.3,"sepal_width":2.9,"petal_length":5.6,"petal_width":1.8,"species":"virginica"},
...         {"sepal_length":6.4,"sepal_width":3.2,"petal_length":4.5,"petal_width":1.5,"species":"versicolor"},
...         {"sepal_length":4.7,"sepal_width":3.2,"petal_length":1.3,"petal_width":0.2,"species":"setosa"}]
>>>
>>>
>>> result = [Iris(**row) for row in DATA]
>>> print(result)  
[Iris(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9, species='virginica'),
 Iris(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2, species='setosa'),
 Iris(sepal_length=5.7, sepal_width=2.8, petal_length=4.1, petal_width=1.3, species='versicolor'),
 Iris(sepal_length=6.3, sepal_width=2.9, petal_length=5.6, petal_width=1.8, species='virginica'),
 Iris(sepal_length=6.4, sepal_width=3.2, petal_length=4.5, petal_width=1.5, species='versicolor'),
 Iris(sepal_length=4.7, sepal_width=3.2, petal_length=1.3, petal_width=0.2, species='setosa')]

2.4.11. Use Case - Complex

Defining complex number by passing keyword arguments directly:

>>> complex(real=3, imag=5)
(3+5j)
>>> number = {'real': 3, 'imag': 5}
>>> complex(**number)
(3+5j)

2.4.12. Use Case - Vector

Passing vector to the function:

>>> def cartesian_coordinates(x, y, z):
...     print(f'{x=} {y=} {z=}')
>>>
>>>
>>> vector = (1, 0, 1)
>>> cartesian_coordinates(*vector)
x=1 y=0 z=1

Passing point to the function:

>>> def cartesian_coordinates(x, y, z):
...     print(f'{x=} {y=} {z=}')
>>>
>>>
>>> point = {'x': 1, 'y': 0, 'z': 1}
>>> cartesian_coordinates(**point)
x=1 y=0 z=1

2.4.13. Use Case - Format

str.format() expects keyword arguments, which keys are used in string. It is cumbersome to pass format(name=name, agency=agency) for every variable in the code. Since Python 3.6 f-string formatting are preferred:

>>> firstname = 'Jan'
>>> lastname = 'Twardowski'
>>> location = 'Moon'
>>>
>>> result = 'Astronaut {firstname} {lastname} on the {location}'.format(**locals())
>>> print(result)
Astronaut Jan Twardowski on the Moon

2.4.14. Use Case - Draw Line

Calling a function which has similar parameters. Passing configuration to the function, which sets parameters from the config:

>>> def draw_line(x, y, color, type, width, markers):
...     pass
>>>
>>>
>>> draw_line(x=1, y=2, color='red', type='dashed', width='2px', markers='disc')
>>> draw_line(x=3, y=4, color='red', type='dashed', width='2px', markers='disc')
>>> draw_line(x=5, y=6, color='red', type='dashed', width='2px', markers='disc')
>>> def draw_line(x, y, color, type, width, markers):
...     pass
>>>
>>>
>>> style = {'color': 'red',
...          'type': 'dashed',
...          'width': '2px',
...          'markers': 'disc'}
>>>
>>> draw_line(x=1, y=2, **style)
>>> draw_line(x=3, y=4, **style)
>>> draw_line(x=5, y=6, **style)

2.4.15. Use Case - Connection

Database connection configuration read from config file:

>>> def database_connect(host, port, username, password, database):
...     pass
>>>
>>>
>>> CONFIG = {
...     'host': 'example.com',
...     'port': 5432,
...     'username': 'myusername',
...     'password': 'mypassword',
...     'database': 'mydatabase'}
>>>
>>> connection = database_connect(
...     host=CONFIG['host'],
...     port=CONFIG['port'],
...     username=CONFIG['username'],
...     password=CONFIG['password'],
...     database=CONFIG['database'])
>>> def database_connect(host, port, username, password, database):
...     pass
>>>
>>>
>>> CONFIG = {
...     'host': 'example.com',
...     'port': 5432,
...     'username': 'myusername',
...     'password': 'mypassword',
...     'database': 'mydatabase'}
>>>
>>> connection = database_connect(**CONFIG)

2.4.16. Use Case - View-Template

Calling function with all variables from higher order function. locals() will return a dict with all the variables in local scope of the function:

>>> def template(template, **user_data):
...     print('Template:', template)
...     print('Data:', user_data)
>>>
>>>
>>> def controller(firstname, lastname, uid=0):
...     groups = ['admins', 'astronauts']
...     permission = ['all', 'everywhere']
...     return template('user_details.html', **locals())
>>>
>>>     # template('user_details.html',
>>>     #    firstname='Jan',
>>>     #    lastname='Twardowski',
>>>     #    uid=0,
>>>     #    groups=['admins', 'astronauts'],
>>>     #    permission=['all', 'everywhere'])
>>>
>>>
>>> controller('Jan', 'Twardowski')  
Template: user_details.html
Data: {'firstname': 'Jan',
       'lastname': 'Twardowski',
       'uid': 0,
       'groups': ['admins', 'astronauts'],
       'permission': ['all', 'everywhere']}

2.4.17. Use Case - Proxy Function

Definition of pandas.read_csv() function https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html

Proxy functions. One of the most common use of *args, **kwargs:

>>> def read_csv(filepath_or_buffer, sep=', ', delimiter=None, header='infer',
...              names=None, index_col=None, usecols=None, squeeze=False, prefix=None,
...              mangle_dupe_cols=True, dtype=None, engine=None, converters=None,
...              true_values=None, false_values=None, skipinitialspace=False,
...              skiprows=None, nrows=None, na_values=None, keep_default_na=True,
...              na_filter=True, verbose=False, skip_blank_lines=True, parse_dates=False,
...              infer_datetime_format=False, keep_date_col=False, date_parser=None,
...              dayfirst=False, iterator=False, chunksize=None, compression='infer',
...              thousands=None, decimal=b'.', lineterminator=None, quotechar='"',
...              quoting=0, escapechar=None, comment=None, encoding=None, dialect=None,
...              tupleize_cols=None, error_bad_lines=True, warn_bad_lines=True,
...              skipfooter=0, doublequote=True, delim_whitespace=False, low_memory=True,
...              memory_map=False, float_precision=None):
...     pass
>>>
>>>
>>> def mycsv(file, encoding='utf-8', decimal=b',',
...           lineterminator='\n', *args, **kwargs):
...
...     return read_csv(file, encoding=encoding, decimal=decimal,
...                     lineterminator=lineterminator, *args, **kwargs)
>>>
>>>
>>> mycsv('iris1.csv')
>>> mycsv('iris2.csv', encoding='iso-8859-2')
>>> mycsv('iris3.csv', encoding='cp1250', verbose=True)
>>> mycsv('iris4.csv', verbose=True, usecols=['Sepal Length', 'Species'])

2.4.18. Use Case - Decorators

Decorators are functions, which get reference to the decorated function as it's argument, and has closure which gets original function arguments as positional and keyword arguments:

>>> def login_required(func):
...     def wrapper(request, *args, **kwargs):
...         if not request.user.is_authenticated():
...             raise PermissionError
...         return func(*args, **kwargs)
...     return wrapper
>>>
>>>
>>> @login_required
... def edit_profile(request):
...     pass

2.4.19. Assignments

Code 2.26. Solution
"""
* Assignment: Unpacking Arguments Define
* Complexity: easy
* Lines of code: 3 lines
* Time: 8 min

English:
    1. Define `result: list[dict]`
    2. Iterate over `DATA` separating `features` from `label`
    3. To `result` append dict with:
       a. key: `label`, value: species name
       b. key: `mean`, value: arithmetic mean of `features`
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj `result: list[dict]`
    2. Iteruj po `DATA` separując `features` od `label`
    3. Do `result` dodawaj dict z:
        * klucz: `label`, wartość: nazwa gatunku
        * klucz: `mean`, wartość: wynik średniej arytmetycznej `features`
    4. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert type(result) is list, \
    'Result must be a list'

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

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [{'label': 'virginica', 'mean': 3.875},
     {'label': 'setosa', 'mean': 2.65},
     {'label': 'versicolor', 'mean': 3.475},
     {'label': 'virginica', 'mean': 6.0},
     {'label': 'versicolor', 'mean': 3.95},
     {'label': 'setosa', 'mean': 4.7}]

"""

DATA = [
    ('Sepal length', 'Sepal width', 'Petal length', 'Petal width', 'Species'),
    (5.8, 2.7, 5.1, 1.9, 'virginica'),
    (5.1, 0.2, 'setosa'),
    (5.7, 2.8, 4.1, 1.3, 'versicolor'),
    (6.3, 5.7, 'virginica'),
    (6.4, 1.5, 'versicolor'),
    (4.7, 'setosa')]


def mean(*args):
    return sum(args) / len(args)


# list[dict]: calculate mean and append dict with {'label': ..., 'mean': ...}
result = ...

Code 2.27. Solution
"""
* Assignment: Unpacking Arguments Range
* Complexity: medium
* Lines of code: 25 lines
* Time: 21 min

English:
    1. Write own implementation of a built-in `myrange(start, stop, step)` function
    2. Note, that function does not take any keyword arguments
    3. How to implement passing only stop argument (`myrange(start=0, stop=???, step=1)`)?
    4. Run doctests - all must succeed

Polish:
    1. Zaimplementuj własne rozwiązanie wbudowanej funkcji `myrange(start, stop, step)`
    2. Zauważ, że funkcja nie przyjmuje żanych argumentów nazwanych (keyword)
    3. Jak zaimplementować możliwość podawania tylko końca (`myrange(start=0, stop=???, step=1)`)?
    4. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * https://github.com/python/cpython/blob/bb3e0c240bc60fe08d332ff5955d54197f79751c/Objects/rangeobject.c#L82
    * use `*args` and `**kwargs`
    * `if len(args) == ...`

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

    >>> assert isfunction(myrange)

    >>> myrange(0, 10, 2)
    [0, 2, 4, 6, 8]

    >>> myrange(0, 5)
    [0, 1, 2, 3, 4]

    >>> myrange(5)
    [0, 1, 2, 3, 4]

    >>> myrange()
    Traceback (most recent call last):
    TypeError: myrange expected at least 1 argument, got 0

    >>> myrange(1,2,3,4)
    Traceback (most recent call last):
    TypeError: myrange expected at most 3 arguments, got 4

    >>> myrange(stop=2)
    Traceback (most recent call last):
    TypeError: myrange() takes no keyword arguments

    >>> myrange(start=1, stop=2)
    Traceback (most recent call last):
    TypeError: myrange() takes no keyword arguments

    >>> myrange(start=1, stop=2, step=2)
    Traceback (most recent call last):
    TypeError: myrange() takes no keyword arguments
"""


# callable: myrange(start=0, stop=???, step=1)
#           note, function does not take keyword arguments
def myrange():
    current = start
    result = []

    while current < stop:
        result.append(current)
        current += step

    return result