Stars

Star Expression in Assignment

What will this do?

>>> numbers = [2, 1, 3, 4]
>>> first, second, rest = numbers
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

How could we get all the remaining values after the first one into a new list?

Here’s one way to do this:

>>> numbers = [2, 1, 3, 4]
>>> first, second, rest = numbers[0], numbers[1], numbers[1:]
>>> first
2
>>> second
1
>>> rest
[1, 3, 4]

Another way:

>>> numbers = [2, 1, 3, 4]
>>> (first, second), rest = numbers[:2], numbers[2:]

Or we can use a * expression during tuple unpacking to capture remaining values into a list:

>>> numbers = [2, 1, 3, 4]
>>> first, second, *rest = numbers
>>> first
2
>>> second
1
>>> rest
[3, 4]

We can use this anywhere in an unpacking assignment:

>>> first, *middle, last = numbers
>>> first
2
>>> middle
[1, 3]
>>> last
4
>>> *rest, last = numbers
>>> rest
[2, 1, 3]
>>> last
4

We cannot use two * operators in the same assignment though because that would be ambiguous:

>>> a, *b, c, *d = numbers
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: two starred expressions in assignment

Stars in List Literals

The other side of the * operator is for unpacking iterables.

The * operator can also be used in list, set, and tuple literals:

>>> numbers = [2, 1, 3, 4]
>>> first, second, *rest = numbers
>>> (0, first, *rest, 9)
(0, 2, 3, 4, 9)
>>> rearranged = [*rest, second, first]
>>> rearranged
[3, 4, 1, 2]
>>> set(numbers) == {*numbers}
True

Argument Unpacking

This * unpacking also works for unpacking iterables into positional arguments when calling functions too:

>>> strings = ["hello", "world"]
>>> print(*strings)
hello world
>>> strings = ["hello", "world"]
>>> print(*strings, sep='\n')
hello
world
>>> print("hi", "there", *strings)
hi there hello world
>>> print(*strings, "hi")
hello world hi

The print function accepts any number of arguments.

The zip function also accepts any number of arguments:

>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> list(zip(*matrix))
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]

Star in Function Arguments

The * unpacking into function arguments has a flip side as well: the * operator can be used for capturing all positional arguments specified to a function.

This allows us to define functions which accept any number of positional arguments:

>>> def print_all_args(*args):
...     for arg in args:
...         print(arg)
...
>>> print_all_args("hello", "world")
hello
world
>>> print_all_args()
>>> print_all_args("hello", "there", "world")
hello
there
world

This can be used with any number of positional arguments:

>>> def greet_all(greeting, *names):
...     for name in names:
...         print(f"{greeting} {name}")
...
>>> greet_all("Hello", "Trey", "Diane")
Hello Trey
Hello Diane
>>> greet_all("Hiya", "Peter", "Gerry", "Trey")
Hiya Peter
Hiya Gerry
Hiya Trey

Keyword-Only Arguments

Any arguments after the * must be given as keyword arguments:

>>> def greet_all(*names, greeting):
...     for name in names:
...         print(f"{greeting} {name}!")
...
>>> greet_all("Hello", "Trey")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: greet_all() missing 1 required keyword-only argument: 'greeting'
>>> greet_all("Trey", "Gerry", greeting="Hello")
Hello Trey!
Hello Gerry!

Arguments after any * expression in a function definition can be thought of as keyword-only arguments.

A lone * can be used to require specific arguments to be keyword-only arguments (without also capturing any remaining positional arguments):

def with_previous(iterable, *, fillvalue=None):
    items = []
    previous = fillvalue
    for item in iterable:
        items.append((item, previous))
        previous = item
    return items

Keyword Arguments

The * operator works for packing and unpacking positional arguments. The ** operator works the same way for keyword arguments.

We can use this operator to capture all keyword arguments passed to a function:

>>> def print_words(**kwargs):
...     for word, count in kwargs.items():
...         print(" ".join([word] * count))
...
>>> print_words(hello=5, world=3)
hello hello hello hello hello
world world world

We can use positional arguments at the same time, but they can only be used before the keyword arguments:

>>> def example(a, **kw):
...     print(a)
...     print(kw)
...
>>> example(4, hello="world")
4
{'hello': 'world'}
>>> def example(*args, **kwargs):
...     print(args)
...     print(kwargs)
...
>>> example(4, 5, multiple="arguments", hello="world")
(4, 5)
{'multiple': 'arguments', 'hello': 'world'}

The ** operator can be used for taking a dictionary and unpacking it for use as keyword arguments:

>>> words = {'hello': 3, 'world': 5}
>>> words
{'hello': 3, 'world': 5}
>>> print_words(**words)
hello hello hello
world world world world world

Just as * operator works for unpacking iterables into list/set/tuple literals, the ** operator works for unpacking dictionaries (and other “mappings”) into dictionary literals:

>>> user = {'name': "Trey", 'website': "https://treyhunner.com"}
>>> defaults = {'name': "Anonymous User", 'page_name': "Profile Page"}
>>> merged = {**user, **defaults}
>>> merged
{'name': 'Anonymous User', 'website': 'https://treyhunner.com', 'page_name': 'Profile Page'}

Argument Unpacking Exercises

Multi-valued Dictionary

This is the dict_from_tuple exercise in dictionaries.py. Edit the dictionaries.py file in the exercises directory to implement this exercise. To test it, run python test.py dict_from_tuple in your exercises directory.

Edit the function dict_from_tuple and modify it to accept a list of tuples of any length and return a dictionary which uses the first item of each tuple as keys and all subsequent items as values.

Note

This exercise is different from (but similar to) dict_from_truple, which you may have seen earlier.

Example usage:

>>> from dictionaries import dict_from_tuple
>>> dict_from_tuple([(1, 2, 3, 4), (5, 6, 7, 8)])
{1: (2, 3, 4), 5: (6, 7, 8)}
>>> dict_from_tuple([(1, 2, 3), (4, 5, 6), (7, 8, 9)])
{1: (2, 3), 4: (5, 6), 7: (8, 9)}

Transpose

This is the transpose exercise in stars.py. Edit the stars.py file in the exercises directory to implement this exercise. To test it, run python test.py transpose in your exercises directory.

Make a function transpose that accepts a list of lists and returns the transpose of the list of lists.

Example usage:

>>> from stars import transpose
>>> transpose([[1, 2], [3, 4]])
[[1, 3], [2, 4]]
>>> matrix = [['a','b','c'],['d','e','f'],['g','h','i']]
>>> transpose(matrix)
[['a', 'd', 'g'], ['b', 'e', 'h'], ['c', 'f', 'i']]

Sum Each

Edit the sum_each function in the stars.py file so that it accepts any number of tuples of numbers and returns tuple of the sums of numbers in corresponding positions (sum of all first items, sum of all second items, etc.)

Example usage:

>>> from stars import sum_each
>>> sum_each((1, 2), (4, 5))
(5, 7)
>>> sum_each((1, 2), (4, 5), (7, 8), (1, 1))
(13, 16)
>>> sum_each((1, 2, 3), (4, 5, 6), (7, 8, 9), (1, 1, 0))
(13, 16, 18)

HTML Tag

This is the html_tag exercise in stars.py. Edit the stars.py file in the exercises directory to implement this exercise. To test it, run python test.py html_tag in your exercises directory.

Make a function that accepts a positional argument and keyword arguments and generates an HTML tag using them.

Example:

>>> from stars import html_tag
>>> html_tag("input", type="email", name="email", placeholder="E-mail address")
'<input type="email" name="email" placeholder="E-mail address">'
>>> img_tag = html_tag("img", src="https://placehold.it/10x10", alt="Placeholder")
>>> img_tag
'<img src="https://placehold.it/10x10" alt="Placeholder">'

Total Length

This is the total_length exercise in iteration.py.

Make a function total length that should calculate the total length of all given iterables.

This function should even work for iterables that don’t work with the built-in len (like zip, enumerate, etc.).

Example:

>>> from iteration import total_length
>>> total_length([1, 2, 3])
3
>>> total_length()
0
>>> total_length([1, 2, 3], [4, 5], iter([6, 7]))
7

Tags Equal

Edit the tags_equal function in slices.py to accept two strings containing opening HTML tags and return True if they have the same attributes with the same values.

Some examples:

>>> from slices import tags_equal
>>> tags_equal("<img src=cats.jpg width=200 height=400>", "<IMG SRC=Cats.JPG height=400 width=200>")
True
>>> tags_equal("<img src=dogs.jpg width=999 height=400>", "<img src=dogs.jpg width=200 height=400>")
False
>>> tags_equal("<p>", "<P>")
True
>>> tags_equal("<b>", "<p>")
False

You can assume:

  1. Attributes don’t have double/single quotes around them

  2. Attributes don’t contain spaces (until you get bonus 3)

  3. Attribute names will not be repeated

  4. All attributes will have values

  5. Attributes have no extra whitespace around them (key=value and never key = value)

Keep in mind that:

  1. Attribute names and values matter, but ordered must be ignored

  2. Attributes are case-insensitive (you’ll need to normalize the case)