Iteration Tools

Zipping

Let’s say we have two lists that are related by their item indexes:

>>> colors = ["blue", "red", "green"]
>>> frequencies = [0.2, 0.3, 0.5]

We’re going to try to make a dictionary from these lists.

Let’s take a look at the zip function:

>>> zip([1, 2], [3, 4])
<zip object at 0x7ff89402ed08>

Zip returns an iterable object, but it’s not a list. Let’s typecast it to a list so we can see what it looks like:

>>> list(zip([1, 2], [3, 4]))
[(1, 3), (2, 4)]

So zip takes the first item from each list and puts them in a tuple, then takes the second item from each list and puts them in a tuple, and so on.

>>> list(zip([1, 2], [3, 4], [5, 6]))
[(1, 3, 5), (2, 4, 6)]

Let’s use zip on our lists:

>>> list(zip(colors, frequencies))
[('blue', 0.2), ('red', 0.3), ('green', 0.5)]

Great. Now remember that a list of two-tuples can be used to construct a dictionary, using the dict constructor:

>>> color_frequencies = dict(zip(colors, frequencies))
>>> color_frequencies
{'blue': 0.2, 'green': 0.5, 'red': 0.3}

We’ve just made a dictionary from our two lists!

Unzipping

What if we want to reverse a zip? Let’s take a closer look at what zip does:

>>> list(zip((0, 1), ('red', 'blue')))
[(0, 'red'), (1, 'blue')]

Now if we zip tuples just like the list we just created:

>>> list(zip((0, 'red'), (1, 'blue')))
[(0, 1), ('red', 'blue')]

If we could unpack the items of our first list into the argument list of the zip function, we could unzip the list and get our original lists back. Let’s see an example of how we could do that:

>>> a = [(0, 1), ('red', 'blue')]
>>> b = list(zip(a[0], a[1]))
>>> b
[(0, 'red'), (1, 'blue')]
>>> c = list(zip(b[0], b[1]))
>>> c
[(0, 1), ('red', 'blue')]
>>> a == c
True

So if we know the length of a list, we can unpack the list by passing individual arguments into the zip function. This only works if we know the length of our list though. And it is tedious and error-prone.

We can use the * prefix operator to unpack all list items into separate arguments to a function. So these two statements are equivalent:

>>> list(zip(b[0], b[1]))
[(0, 1), ('red', 'blue')]
>>> list(zip(*b))
[(0, 1), ('red', 'blue')]

Here is another example of zipping & unzipping to get the original lists back:

>>> nums = [1, 2, 3]
>>> colors = ['red', 'blue', 'green']
>>> zipped = list(zip(nums, colors))
>>> zipped
[(1, 'red'), (2, 'blue'), (3, 'green')]
>>> nums2, colors2 = zip(*zipped)
>>> list(nums2) == nums
True
>>> list(colors2) == colors
True

Let’s take the items from a dictionary:

>>> color_frequencies = {'blue': 0.2, 'green': 0.5, 'red': 0.3}
>>> color_frequencies.items()
dict_items([('blue', 0.2), ('green', 0.5), ('red', 0.3)])

We can unpack this list of tuples into a call to the zip function to unzip the keys and values:

>>> colors, frequencies = zip(*color_frequencies.items())
>>> colors
('blue', 'green', 'red')
>>> frequencies
(0.2, 0.5, 0.3)

We wouldn’t usually need to use zip in this way on a dictionary though because we could get the keys and values using methods instead:

>>> color_frequencies.keys()
dict_keys(['blue', 'green', 'red'])
>>> color_frequencies.values()
dict_values([0.2, 0.5, 0.3])

Zip Exercises

Parse two-line CSV data

Make a function parse_row that accepts a string consisting of two lines of comma-separated data and parses it, returning a dictionary where the keys are elements from the first row and the values are from the second row.

Example usage:

>>> color_data = "purple,indigo,red,blue,green\n0.15,0.25,0.3,0.05,0.25"
>>> parse_row(color_data)
{'blue': '0.05', 'green': '0.25', 'purple': '0.15', 'red': '0.3', 'indigo': '0.25'}

Double-valued Dictionary

Make a function dict_from_truple that accepts a list of three-item tuples and returns a dictionary where the keys are the first item of each tuple and the values are a two-tuple of the remaining two items of each input tuple.

Example usage:

>>> dict_from_truple([(1, 2, 3), (4, 5, 6), (7, 8, 9)])
{1: (2, 3), 4: (5, 6), 7: (8, 9)}

Bonus: Multi-valued Dictionary

Rewrite dict_from_truple 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.

Example usage:

>>> dict_from_truple([(1, 2, 3, 4), (5, 6, 7, 8)])
{1: (2, 3, 4), 5: (6, 7, 8)}
>>> dict_from_truple([(1, 2, 3), (4, 5, 6), (7, 8, 9)])
{1: (2, 3), 4: (5, 6), 7: (8, 9)}

Star Expression in Assignment

The * operator can be used in a couple different ways.

The first use for * is for packing a sequence of items into a list in a single variable.

This can be used to capture remaining values in tuple unpacking:

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

We can use this anywhere in an unpacking assignment:

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

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

>>> a, *b, c, *d = numbers
  File "<stdin>", line 1
SyntaxError: two starred expressions in assignment

Star in Function Arguments

This operator is also very commonly used for allowing functions to 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("{} {}".format(greeting, name))
...
>>> greet_all("Hello", "Trey", "Diane")
Hello Trey
Hello Diane
>>> greet_all("Hiya", "Peter", "Gerry", "Trey")
Hiya Peter
Hiya Gerry
Hiya Trey

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

>>> def greet_all(*names, greeting):
...     for name in names:
...         print("{} {}!".format(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!

Argument Unpacking

The other use for * is for taking an iterable and unpacking it into arguments in a function call.

This is what we did with the zip function earlier.

>>> strings = ["hello", "world"]
>>> print_all_args(*strings)
hello
world

Positional arguments may go before argument unpacking, but not after:

>>> print_all_args("hi", "there", *strings)
hi
there
hello
world
>>> print_all_args(*strings, "hi")
  File "<stdin>", line 1
SyntaxError: only named arguments may follow *expression

Only keyword arguments can come after an argument unpacking:

>>> names = ["Patricia", "Bryan", "Michelle"]
>>> greet_all(*names, greeting="Greetings")
Greetings Patricia!
Greetings Bryan!
Greetings Michelle!

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)
{'hello': 'world', 'multiple': 'arguments'}

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

Argument Unpacking Exercises

Product

Write a product function that takes any number of arguments and multiplies them together, returning the result. Example:

>>> product(10)
10
>>> product(5, 6, 8)
240

Unzip

Create a function unzip that does the opposite of zip.

Example:

>>> unzip([(0, 1), (2, 3)])
[(0, 2), (1, 3)]

HTML Tag

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

Example:

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