Chapter 19 · pp. 205–214
The Goodies
Python has many optional features. You can write good programs without them, but they often make code shorter, easier to read, or faster. This chapter covers one-line choices, one-line list building, lazy value generators, sets, counting tools, automatic dictionary values, and flexible keyword arguments.
Choosing a value in one expression
A conditional expression picks one of two values inside a single expression. Its form is a if condition else b. It gives a when the condition is true and b otherwise (p. 205).
Here is an example. Taking the log of a number that is zero or negative raises a ValueError. This line avoids the crash:
y = math.log(x) if x > 0 else float('nan')
It reads almost like English: y gets the log if x is positive, and otherwise gets NaN, the special "Not a Number" value (p. 205).
You can swap an if/else statement for a conditional expression only when both branches are simple expressions. They must either be returned or be assigned to the same variable (pp. 205–206). Two examples:
- A recursive factorial becomes
return 1 if n == 0 else n * factorial(n-1) (p. 206).
- An optional-argument default can be set in one line:
self.pouch_contents = [] if contents == None else contents. This is useful because a default of None is replaced by a fresh empty list (p. 206).
If a branch needs several statements, it cannot be shortened this way.
Building lists and values compactly
A list comprehension builds a new list in one expression. Square brackets show that a list is being made. Inside them go an element expression and a for clause, and optionally an if (pp. 206–207).
- Mapping:
[s.capitalize() for s in t] does the work of a loop that appends each capitalized string to a result list (p. 206).
- Filtering:
[s for s in t if s.isupper()] keeps only the upper-case strings (pp. 206–207).
The syntax can feel backward. The loop variable s appears in the expression before the for clause defines it (p. 206).
Comprehensions are usually shorter and faster than the equivalent loops, sometimes much faster. The cost is debugging. The loop is hidden inside the brackets, so you cannot add a print statement to watch it run. Use comprehensions only for computations simple enough to get right the first time. A plain loop is easier to inspect step by step (p. 207).
A generator expression looks the same but uses parentheses. It produces a generator object that does not compute everything at once. It waits until it is asked for a value (p. 207).
- With
g = (x**2 for x in range(5)), nothing is computed yet.
next(g) returns 0, then 1, and so on.
- A
for loop over g continues from where next stopped, because the generator remembers its position. It prints 4, 9, and 16 after the first two values have been taken.
- After the last value,
next(g) raises StopIteration. It keeps raising it every time after that.
Generators pair well with sum, max, and min. For example, sum(x**2 for x in range(5)) gives 30 (p. 207).
any and all
The built-in any returns True if at least one value in a sequence is True. The built-in all returns True only if every value is True (pp. 207–208). For example, any([False, False, True]) is True.
They are often used with generator expressions. any(letter == 't' for letter in 'monty') is True. That particular test is no better than the in operator, but the idea scales up. A function that checks that a word avoids forbidden letters can be written as:
not any(letter in forbidden for letter in word)
This reads as "no letter of the word is forbidden." It is also efficient. With a generator expression, any stops at the first True value instead of checking the whole sequence (p. 208).
Sets
A set is a built-in type that acts like a collection of dictionary keys with no values. Each element appears only once. Adding elements and testing membership are fast. Sets also have operators, such as - for difference and <= for "is a subset of" (p. 208).
A word-list subtract function used to build a dictionary full of useless None values. With sets it is just set(d1) - set(d2). The result is a set, but iterating over it works the same way (p. 208).
The subset operator can also replace a loop. set(word) <= set(available) is True when every letter of word is in available, including when the two sets are equal (p. 209).
Testing a list for duplicates. Compare the size of the set made from the list with the size of the list (p. 209):
len(set(t)) < len(t)
- A set keeps each element only once.
- If any element repeats, the set has fewer items than the list.
- For
[1, 2, 2, 3], the set has 3 items and the list has 4, so the result is True.
- With no repeats, the two lengths are equal and the result is False.
Counter and defaultdict
A Counter, from the collections module, is like a set that also records how many times each element appears. It represents a multiset. You can build one from a string, a list, or anything you can iterate over. The keys must be hashable (p. 209).
Counter('parrot') maps 'r' to 2 and every other letter to 1.
- Looking up a missing key gives 0 instead of an error. For example,
count['d'] is 0 (pp. 209–210).
- Two words are anagrams if their Counters are equal:
Counter(word1) == Counter(word2) (p. 210).
most_common returns (value, frequency) pairs from most to least frequent. count.most_common(3) on 'parrot' starts with r and 2 (p. 210).
- Counters also support set-like operations, such as addition, subtraction, union, and intersection (p. 210).
A defaultdict is a dictionary that creates a value automatically when you access a missing key. When you create it, you give it a factory, a function that makes new values (pp. 210–211).
In d = defaultdict(list), the argument is the class list, not list(), which would be a new list. The factory is called only when a missing key is accessed. Then d['new key'] creates an empty list and stores it in d. If you change that list, the change shows up in the dictionary (p. 210).
This simplifies building a dictionary of lists. Compare the options for grouping anagrams (p. 211):
- An
if t not in d check works but takes several lines.
d.setdefault(t, []).append(word) is shorter, but it makes a new list every time, even when one is not needed. That may be costly if the factory is complicated.
d[t].append(word) with a defaultdict is the simplest. It makes a new object only when one is actually needed.
Gathering and scattering keyword arguments
A *args parameter collects only positional arguments, those without keywords, into a tuple. A call like printall(1, 2.0, third='3') causes a TypeError because third is a keyword argument (pp. 212–213).
To collect keyword arguments too, add a ** parameter, usually named kwargs:
def printall(*args, **kwargs):
Now kwargs becomes a dictionary, {'third': '3'}, and args holds (1, 2.0) (p. 213).
The reverse also works. In a function call, **d spreads a dictionary into keyword arguments. With d = dict(x=1, y=2), the call Point(**d) means Point(x=1, y=2). Without the **, Point(d) passes the whole dictionary as a single positional argument. It is assigned to x, and the call fails because y is missing (p. 213).
This is handy for functions with many parameters. You can keep commonly used options in a dictionary and pass them along (p. 213).
Check yourself
- Rewrite this statement as one conditional expression:
if n > 10: label = 'big' followed by else: label = 'small'.
- What is the difference between
[x*2 for x in range(3)] and (x*2 for x in range(3))? What happens when you call next on the second one four times?
- Why is
len(set(t)) < len(t) a correct test for duplicates in t? What does it give for [5, 5, 7]?
- What does
Counter('banana')['z'] return, and how is this different from a normal dictionary?
- A function is defined as
def f(*args, **kwargs). What do args and kwargs hold after the call f(1, 2, a=3)?