Built-in helpers, itertools, bisect, and functools¶
~25 min ยท prerequisite: Deques and heaps
What you will be able to do¶
- Use
zip,enumerate, andmaxwith akey, and know how each handles uneven inputs. - Use
itertoolsfor running totals and combinations, andbisectto count values in a sorted list. - Use
functools.cacheto remember results, and the exact integer functions inmath.
The idea¶
Python ships with helpers that say what you mean in one line, and they are often faster than a hand-written loop. Reach for them before you write the loop yourself. Each helper has a short, precise definition in the library reference, so you can check its behaviour there. The built-in zip(a, b) pairs items by position and stops at the shorter input, so extra items are dropped silently. The built-in enumerate(a, start=1) pairs each item with a counter. The built-in max(a, key=f) returns the item with the largest key, and on a tie it returns the first such item.
The module itertools provides iterators, which produce items one at a time, so you usually wrap them in list(...) to see the values. The function accumulate gives running totals, which are prefix sums. The function combinations(a, 2) gives each pair of positions once. The function groupby groups only neighbouring equal items, so sort the data first.
The module bisect searches a sorted list. The call bisect_left(a, x) returns the position before any items equal to x, and bisect_right(a, x) returns the position after them. Both take logarithmic time. The difference of two calls counts the values in a range. bisect does not check the order of the list, so on unsorted input it gives wrong answers without any error.
The module functools has cache, which remembers the results of a function, so a recursive function does not repeat work. The module math has isqrt, which gives the exact integer square root. Prefer it to int(n ** 0.5), because floating-point values can be off for large numbers.
Worked example¶
from itertools import accumulate
from bisect import bisect_left, bisect_right
values = [3, 1, 4, 1, 5]
print(list(accumulate(values)))
ordered = sorted(values)
lo = bisect_left(ordered, 1)
hi = bisect_right(ordered, 4)
print(ordered[lo:hi])
The output is:
Line by line, this is what Python does:
accumulate(values)yields 3, then 3 + 1, then 3 + 1 + 4, and so on.listcollects the running sums.sorted(values)makes the sorted list[1, 1, 3, 4, 5]thatbisectneeds.bisect_left(ordered, 1)returns0, the position before the first1, andbisect_right(ordered, 4)returns4, the position after the4.- The slice
ordered[lo:hi]takes positions 0 to 3, which hold the values from 1 to 4.
Try it¶
The first block pairs items. The second block counts values in a range and uses cache and the math functions.
from bisect import bisect_left, bisect_right
def pair_up(a, b):
return list(zip(a, b))
assert pair_up([1, 2, 3], "abc") == [(1, "a"), (2, "b"), (3, "c")]
assert pair_up([1, 2, 3], [9]) == [(1, 9)]
assert list(enumerate("xy", start=1)) == [(1, "x"), (2, "y")]
def count_in_range(sorted_values, lo, hi):
return bisect_right(sorted_values, hi) - bisect_left(sorted_values, lo)
assert count_in_range([1, 3, 3, 5, 8, 9], 3, 8) == 4
assert count_in_range([1, 3, 3, 5, 8, 9], 10, 20) == 0
Common mistakes¶
-
Expecting
zipto complain about length. The calllist(zip([1, 2, 3], [4]))returns[(1, 4)]with no message. Addstrict=Trueto get: -
Grouping unsorted data. The call
[(k, len(list(g))) for k, g in groupby([1, 2, 1])]returns[(1, 1), (2, 1), (1, 1)], so the key1appears twice. Callsorted(...)first. -
Searching an unsorted list with
bisect. The callbisect_left([3, 1, 2], 1)returns0, although the1is at index 1. No error appears. Sort the list first. -
Passing a list to a cached function. Calling a function decorated with
@cacheasf([1])gives:Pass a tuple, such as
f((1,)).
Practice¶
Write the function named in each problem. The samples are checks.
-
Running totals (easy). Write
running_totals(values), which returns the prefix sums. Sample:running_totals([3, 1, 4])returns[3, 4, 8]. -
Index of the largest (easy). Write
index_of_max(values). Return the first index of the largest value. Sample:index_of_max([2, 9, 4, 9])returns1. -
Count in a range (medium). Write
count_in_range(values, lo, hi). The input is not sorted. Count the valuesvwithlo <= v <= hi. Sample:count_in_range([8, 1, 5, 3, 9, 3], 3, 8)returns4. -
Even products (medium). Write
even_pairs(values). Count the pairs of positionsi < jfor whichvalues[i] * values[j]is even. Sample:even_pairs([1, 2, 3, 4])returns5. -
Sum of gcd values (stretch). Write
gcd_sum(n). Return the sum ofgcd(i, j)over all pairs1 <= i < j <= n. Samples:gcd_sum(3)returns3, andgcd_sum(4)returns7.
Solution
- ```python from itertools import accumulate
def running_totals(values): return list(accumulate(values))
assert running_totals([3, 1, 4]) == [3, 4, 8] ```
- ```python def index_of_max(values): return max(range(len(values)), key=lambda i: values[i])
assert index_of_max([2, 9, 4, 9]) == 1 ```
max returns the first item with the largest key, so the earlier index wins a tie.
- ```python from bisect import bisect_left, bisect_right
def count_in_range(values, lo, hi): ordered = sorted(values) return bisect_right(ordered, hi) - bisect_left(ordered, lo)
assert count_in_range([8, 1, 5, 3, 9, 3], 3, 8) == 4 ```
- ```python from itertools import combinations
def even_pairs(values): return sum(1 for a, b in combinations(values, 2) if (a * b) % 2 == 0)
assert even_pairs([1, 2, 3, 4]) == 5 ```
- ```python from itertools import combinations from math import gcd
def gcd_sum(n): return sum(gcd(i, j) for i, j in combinations(range(1, n + 1), 2))
assert gcd_sum(3) == 3 assert gcd_sum(4) == 7 ```
This checks every pair, so the work grows with the square of n. It is a correct first version.
Recap¶
zipstops at the shorter input,enumerateadds positions, andmaxreturns the first item among ties.itertoolsreturns iterators, so wrap them inlist(...)to see values.bisectneeds a sorted list.functools.cachememoises a function with hashable arguments, andmath.isqrtgives an exact integer square root.