Itertools and Functools
Two standard library modules built specifically around functional-style, iterator-based programming.
itertools: Iterator Building Blocks
Infinite Iterators
import itertools
itertools.count(10, 2) # 10, 12, 14, 16, ... forever
itertools.cycle([1, 2, 3]) # 1, 2, 3, 1, 2, 3, ... forever
itertools.repeat("x", 3) # 'x', 'x', 'x'Combinatoric Generators
list(itertools.permutations([1, 2, 3]))
# [(1,2,3),(1,3,2),(2,1,3),(2,3,1),(3,1,2),(3,2,1)]
list(itertools.permutations([1, 2, 3], 2)) # length-2 permutations
# [(1,2),(1,3),(2,1),(2,3),(3,1),(3,2)]
list(itertools.combinations([1, 2, 3], 2)) # order doesn't matter, no repeats
# [(1,2),(1,3),(2,3)]
list(itertools.combinations_with_replacement([1, 2, 3], 2))
# [(1,1),(1,2),(1,3),(2,2),(2,3),(3,3)]
list(itertools.product([1, 2], ["a", "b"])) # cartesian product
# [(1,'a'),(1,'b'),(2,'a'),(2,'b')]
list(itertools.product([0, 1], repeat=3)) # all 3-bit combinationsChaining and Grouping
list(itertools.chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5], flattens multiple iterables
data = [1, 1, 2, 2, 2, 3, 1]
for key, group in itertools.groupby(data):
print(key, list(group))
# 1 [1, 1]
# 2 [2, 2, 2]
# 3 [3]
# 1 [1]
groupbyonly groups CONSECUTIVE equal elementsNotice
1appears in two separate groups above, because the data was not sorted first. Alwayssort()the input by the same key before usinggroupby()if you want ALL matching elements grouped together, not just runs of adjacent matches.
list(itertools.islice(itertools.count(), 5)) # [0, 1, 2, 3, 4], slice an infinite iterator
list(itertools.zip_longest([1, 2, 3], ["a", "b"], fillvalue="?"))
# [(1,'a'), (2,'b'), (3,'?')]functools: Higher-Order Function Tools
reduce
from functools import reduce
reduce(lambda acc, x: acc + x, [1, 2, 3, 4]) # 10
reduce(lambda acc, x: acc * x, [1, 2, 3, 4], 1) # 24, factorial-like with initial valuepartial: Pre-Filling Arguments
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
square(5) # 25
cube(5) # 125
partialvslambda
partial(power, exponent=2)is more explicit and slightly more efficient thanlambda x: power(x, exponent=2), and it preserves better introspection (the underlying function is still accessible via.func).
lru_cache: Memoization
Already covered in depth in Decorators, the short version:
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive(n):
...wraps: Preserving Metadata in Decorators
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrappercached_property: Lazy, Cached Instance Attribute
from functools import cached_property
class DataProcessor:
def __init__(self, data):
self.data = data
@cached_property
def summary(self):
print("Computing summary...")
return sum(self.data)
dp = DataProcessor([1, 2, 3])
dp.summary # prints 'Computing summary...', returns 6
dp.summary # returns 6 immediately, NOT recomputed, cached on the instancetotal_ordering
from functools import total_ordering
@total_ordering
class Money:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
return self.amount == other.amount
def __lt__(self, other):
return self.amount < other.amount
# total_ordering auto-fills in __le__, __gt__, __ge__ from the two methods abovesingledispatch: Function Overloading by Argument Type
from functools import singledispatch
@singledispatch
def process(value):
print(f"Generic: {value}")
@process.register
def _(value: int):
print(f"Integer: {value}")
@process.register
def _(value: str):
print(f"String: {value}")
process(42) # Integer: 42
process("hi") # String: hi
process(3.14) # Generic: 3.14