Chapter 33, Intermediate
Higher-order functions
Functions that take functions, or return functions. This is where sorting, filtering, and transforming data becomes expressive.
Code for this chapter: intermediario/cap33_ordem_superior.py
lambda and the sorting key
A lambda is an anonymous function of a single expression. The most common use is as an argument key to sorted, min and max, which say by which criterion to compare:
pessoas = [("Ana", 31), ("Bia", 25), ("Caio", 31)]
print(sorted(pessoas, key=lambda p: p[1]))
print(sorted(pessoas, key=lambda p: (-p[1], p[0])))
print(max(pessoas, key=lambda p: p[1]))
[('Bia', 25), ('Ana', 31), ('Caio', 31)]
[('Ana', 31), ('Caio', 31), ('Bia', 25)]
('Ana', 31)
Python's sorting is stable: items with the same key maintain their original order. And a tuple as a key sorts by multiple criteria; the minus sign inverts the order of a numeric field.
For simple keys, the module operator avoids the lambda:
from operator import itemgetter
produtos = [
{"nome": "caneta", "preco": 3.5},
{"nome": "caderno", "preco": 18.9},
{"nome": "lápis", "preco": 1.2},
]
print([p["nome"] for p in sorted(produtos, key=itemgetter("preco"))])
['lápis', 'caneta', 'caderno']
map, filter, and zip
The map applies a function to each item and the filter keeps the items that pass a test. The two are lazy: they return iterators, and the list() is the one that consumes:
numeros = [1, 2, 3, 4, 5]
dobrados = list(map(lambda n: n * 2, numeros))
pares = list(filter(lambda n: n % 2 == 0, numeros))
print(dobrados, pares)
print(type(map(str, numeros)).__name__)
[2, 4, 6, 8, 10] [2, 4]
map
In practice, I prefer comprehensions to map and filter with lambda, because they say the same thing in a more readable way. The map still applies when the function already exists (map(str, numeros)).
The zip joins sequences in pairs and stops at the shortest one, without warning. Since Python 3.10, the parameter strict=True turns this silence into an error:
nomes = ["Ana", "Bia", "Caio"]
notas = [9, 8]
print(list(zip(nomes, notas)))
try:
list(zip(nomes, notas, strict=True))
except ValueError as erro:
print(erro)
[('Ana', 9), ('Bia', 8)]
zip() argument 2 is shorter than argument 1
reduce and partial
The reduce accumulates a sequence into a single value. The partial "freezes" some arguments of a function and returns a new one:
from functools import partial, reduce
import operator
def potencia(base, expoente):
return base ** expoente
quadrado = partial(potencia, expoente=2)
cubo = partial(potencia, expoente=3)
print(reduce(operator.mul, [1, 2, 3, 4], 1))
print(quadrado(5), cubo(2))
24
25 8
Do not assign a lambda to a name
PEP 8 recommends that a named function be defined with
def, and not withquadrado = lambda x: x ** 2. The reason is practical: thedefnames the function in error messages and allows for a docstring. The lambda is for inline use, as an argument.
Exercise 1
Sort students by two criteria
Write ordenar_alunos(alunos) that receives a list of dictionaries with nome and nota and sorts by the highest grade and, in case of a tie, by name.