Python Comprehensions
Table of Contents
A comprehension is convenient way to write combinations of maps and filters
over some of the standard Python data types. We begin by looking at the basic
forms of the if and for statements. Both of these expressions have more
complex forms which we will see later.
Mapping and filtering with if and for
We can use a
forloop to do a kind of mapping. Suppose we want to double every number in a list of numbers.def double_list(ns): doubles = [] for n in ns: doubles.append(2 * n) return doubles >>> double_list([1,2,3]) [2, 4, 6]
We can combine
forwithifto filter out all negative numbers from a list of numbersdef drop_negatives(ns): nonnegs = [] for n in ns: if n >= 0: nonnegs.append(n) return nonnegs >>> drop_negatives([-2, -1, 0, 1, 2]) [0, 1, 2]
We can nest
forloopsdef cross_product(xs, ys): tuples = [] for x in xs: for y in ys: tuples.append((x, y)) return tuples >>> cross_product([1,2,3], [4,5]) [(1, 4), (1, 5), (2, 4), (2, 5), (3, 4), (3, 5)]
And we can combine nested looping with a filter to get all pairs (2-tuples) where the first element is positive and the second negative.
def posnegs(xs, ys): pns = [] for x in xs: for y in ys: if x > 0 and y < 0: pns.append((x,y)) return pns >>> posnegs([1, -2, 3, -4], [5, -6, 7, -8]) [(1, -6), (1, -8), (3, -6), (3, -8)]
Comprehensions
A comprehension is an abbreviated way to write such combinations of mapping
and filtering with if and for. It turns the code from the previous section
inside out and obviates the need for a temporary variable.
We will first look at comprehensions over lists in some detail and then see how to make comprehensions for sets and dictionaries.
List Comprehensions
We can write the list of doubles as
def double_list(ns): return [2 * n for n in ns]
The list comprehension has several advantages:
- It is more compact.
- It expresses directly the list we want.
- There is no need to first set a variable and then update it; this takes place behind the scenes.
- The loop variable
doublesis not left 'lying around' after theforloop. Although there are cases where we might make use of the value of the loop variable after completion of theforloop, we generally do not and so, strictly speaking, shoulddelit.
Similarly we can drop the negatives from a list as
def drop_negatives(ns): return [n for n in ns if n >= 0] >>> drop_negatives([-2, -1, 0, 1, 2]) [0, 1, 2]
The cross product becomes
def cross_product(xs, ys): return [(x,y) for x in xs for y in ys] >>> cross_product([1,2,3], [4,5]) [(1, 4), (1, 5), (2, 4), (2, 5), (3, 4), (3, 5)]
And the nested loop with filter becomes
def posnegs(xs, ys): return [(x,y) for x in xs for y in ys if x > 0 and y < 0] >>> posnegs([1, -2, 3, -4], [5, -6, 7, -8]) [(1, -6), (1, -8), (3, -6), (3, -8)]
Note how the deeply nested
ifwithin aforwithin aforgets flattened into a succinct, readable form.To aid readability further the clauses can be placed under one another
def posnegs(xs, ys): return [(x, y) for x in xs for y in ys if x > 0 and y < 0]
Although all of the above examples are on lists of numbers, comprehensions work on lists of all data types, e.g. strings. If we are given a list of strings and asked to make a list of the lengths of those strings, we might use a
forloop as follows:strings = ['a', 'bc', 'def', 'ghijklmn'] lengths = [] for string in strings: lengths.append(len(string))
Now
lengthscontains the list[1, 2, 3, 8]
The list comprehension compacts the
forloop into a single linelengths = [len(string) for string in strings]
This can be read as "the list of
len(string)withstringtaking, in turn, the value of each element ofstrings".Here is an example of filtering a list of numbers and taking the square root of only the positive numbers.
import math math.sqrt(4) ns = [1, -2, 3, -4, 5, -6] roots = [math.sqrt(n) for n in ns if n > 0]
In this example we first
importthemathlibrary and show how to use thesqrtfunction. Then we return a list of the square roots of only the positive elements of the listns.Using a
forloop instead of a comprehension, we might write:roots = [] for n in ns: if n > 0: roots.append(math.sqrt(n))
This example flattens a list of lists of numbers.
elements = [x for l in [[1,2,3], [4,5,6], [7,8,9]] for x in l]
- More examples can be found in the documentation.
Set Comprehension
In the same way as we can write list comprehensions, so too can we write set
comprehensions. We simply write braces {} instead of square brackets []
{x for x in [1,2,3,2,1]}
{c for c in 'abacadabra'}
Note how the latter example iterates over the characters in a string.
Filtering, nesting and multiple iteration are also possible.
>>> {letter for letter in 'abstemiously' if letter not in 'aeiou'}
{'b', 'l', 'm', 's', 't', 'y'}
Dictionary Comprehension
In the same way as we write set comprehensions, so too can we write dictionary comprehensions, for example to map strings to their lengths.
{s:len(s) for s in ['a', 'bc', 'def', 'ghijklm']}
An equivalent for loop is:
d = {} for s in ['a', 'bc', 'def']: d[s] = len(s)
We can also map across a dictionary
d = {"a": 3, "b": 5, "c": 7, "d": 9} [k for k in d if d[k] > 6]
Generators
Since we can write comprehensions for lists, sets and dictionaries, one might wonder whether we can write tuple comprehensions. With a moment's thought we realise tuples are immutable so we cannot build a tuple as follows:
# NOT ALLOWED! tuples are immutable t = () for x in [1, 2, 3]: t.append(x)
And if we try to build a tuple using a comprehension, we get a strange looking result:
>>> (x for x in [1,2,3]) <generator object <genexpr> at 0x7f28be212620>
This result is actually a generator, an object that we can iterate over. Generators are dealt with in the chapter on Control Flow