Programação funcional

Funções livres de efeitos, ordem superior, lambda, closures, map, filter e reduce.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Funções livres de efeitos

Uma função pura devolve o mesmo resultado para os mesmos argumentos e não tem efeitos observáveis fora do cálculo. Ler a hora, pedir entrada, imprimir, alterar uma lista recebida ou atualizar estado global quebra essa condição.

sorted(notas) cria outra lista, enquanto notas.sort() altera a existente. Uma solução livre de efeitos devolve os dados transformados e deixa a apresentação para outra parte do programa.

def com_bonus(notas, bonus):
    resultado = []
    for nota in notas:
        resultado.append(nota + bonus)
    return resultado

notas = [10, 14]
print(com_bonus(notas, 2))
print(notas)

A saída é [12, 16] e [10, 14]; o ciclo calcula nota + bonus para cada entrada e constrói outra lista, como a compreensão equivalente da lição de compreensões.

Observa o contrato: cada resultado depende dos argumentos e as notas originais permanecem iguais.

Python permite misturar estilos. Se um exercício exige um estilo sem atribuições nem mutação, esse requisito é mais restrito do que a pureza. Nesse caso, usa operações sobre valores e funções de ordem superior em vez de atualizar uma coleção.

Funções são valores

O nome de uma função pode ser guardado, passado ou devolvido. quadrado é o objeto função; quadrado(3) é o resultado da chamada. Passar quadrado(3) onde se espera uma função passa o número 9 e impede a chamada posterior.

def quadrado(x):
    return x * x

def aplicar_duas_vezes(funcao, x):
    return funcao(funcao(x))

print(aplicar_duas_vezes(quadrado, 2))

O resultado é 16, pois 2 passa primeiro a 4 e depois a 16. Uma função que recebe ou devolve outras funções é uma função de ordem superior, e um predicado é uma função usada para decidir se um valor satisfaz uma condição, como x > 0.

Funções de ordem superior e lambda

lambda x: x * x cria uma função com uma expressão como corpo. Não é uma instrução para executar já. Usa def quando precisas de vários passos ou quando dar um nome à operação ajuda a ler.

Ordenar por um campo
alunos = [("Ana", 15), ("Bruno", 12), ("Carla", 15)]
ordenados = sorted(alunos, key=lambda aluno: (-aluno[1], aluno[0]))
print(ordenados)
print(alunos)
Resultado

Executa o código para ver a saída e os gráficos.

A chave é um tuplo: Python compara primeiro o primeiro campo, só usando o segundo em caso de empate.

AlunoChave
("Ana", 15)(-15, "Ana")
("Bruno", 12)(-12, "Bruno")
("Carla", 15)(-15, "Carla")

Como -15 é menor que -12, o negativo da nota põe as notas maiores primeiro e o nome desempata por ordem crescente; a entrada permanece igual. sorted é estável: elementos com chaves iguais conservam a ordem relativa que tinham na entrada.

Map, filter e reduce

map(f, xs) aplica f a cada elemento, enquanto filter(p, xs) conserva os elementos para os quais o predicado p dá um valor verdadeiro. Em Python 3, ambos devolvem iteradores, como os da lição de compreensões, e list materializa os resultados, por isso depois de converteres um filter em lista uma segunda conversão dá [].

reduce, de functools, combina os elementos da esquerda para a direita com um acumulador: com valor inicial z e elementos a, b, c, calcula f(f(f(z, a), b), c). Esse percurso também se chama fold à esquerda; sem valor inicial, a primeira entrada torna-se o acumulador e a sequência vazia causa TypeError.

Selecionar, transformar e acumular
from functools import reduce

valores = [1, 2, 3, 4, 5]
impares = filter(lambda x: x % 2 != 0, valores)
quadrados = map(lambda x: x * x, impares)
soma = reduce(lambda acumulado, x: acumulado + x, quadrados, 0)
print(soma)
print(valores)
print(reduce(lambda a, x: a + x, [], 0))
Resultado

Executa o código para ver a saída e os gráficos.

Segue o exemplo com valores = [1, 2, 3, 4, 5]:

EtapaEntrada consumidaSaída
filter ímpares1, 2, 3, 4, 51, 3, 5
map quadrados1, 3, 51, 9, 25
reduce soma de 01, 9, 251, 10, 35

Os iteradores não guardam estas listas intermédias: quando reduce pede o próximo quadrado, map pede o próximo ímpar a filter, que avança na entrada até o encontrar.

A tabela separa as etapas para mostrar os valores, mas uma chamada a list(impares) antes do cálculo consumiria o filtro e deixaria a redução sem termos.

O valor inicial também define a resposta para a entrada vazia. Para somar, sum é uma alternativa direta; aqui usamos reduce para seguir o acumulador.

A ordem importa se a operação não for associativa. reduce(lambda a, x: a - x, [2, 3], 10) calcula (10 - 2) - 3, que vale 5. Não calcula 10 - (2 - 3).

Any, all e condições sobre coleções

any decide se existe pelo menos um valor verdadeiro num iterável; all decide se todos os valores são verdadeiros. Ambas param logo que a resposta fica determinada: any no primeiro verdadeiro, all no primeiro falso. any([]) é False, pois não há testemunho; all([]) é True, pois não há um elemento que viole a condição. Uma regra que também exige pelo menos um elemento precisa de verificar essa existência separadamente.

Suponhamos que cada caixa guarda um conjunto de peças. Queremos saber se cada peça pedida existe em alguma caixa, podendo usar caixas diferentes:

caixas = [{"parafuso"}, {"porca", "anilha"}]
pedidas = ["parafuso", "porca"]
print(all(any(peca in caixa for caixa in caixas) for peca in pedidas))

A saída é True. Para parafuso há uma caixa que o contém; para porca também, ainda que seja outra. Trocar a ordem para any(all(peca in caixa for peca in pedidas) for caixa in caixas) pede uma caixa que contenha todas as peças e dá False neste exemplo. A ordem dos percursos exprime duas condições diferentes.

Se não há peças pedidas, a primeira condição é verdadeira mesmo sem caixas. Com peças pedidas e nenhuma caixa, é falsa. Usámos listas e conjuntos que permitem novos percursos; reutilizar um único iterador de caixas já consumido poderia perder candidatos na segunda peça.

Devolver funções e âmbito léxico

Uma função interior pode conservar acesso ao contexto onde foi criada. Chamamos closure a essa combinação de função e contexto.

Construir funções com um parâmetro fixo
def multiplicador(fator):
    def multiplicar(x):
        return fator * x
    return multiplicar

dobro = multiplicador(2)
triplo = multiplicador(3)
print(dobro(5), triplo(5))
Resultado

Executa o código para ver a saída e os gráficos.

As duas funções conservam fatores diferentes. return multiplicar devolve a função, sem a chamar. Depois de multiplicador regressar, o fator de cada contexto continua acessível.

A closure consulta as associações dos nomes conservados. Não faz uma cópia automática dos seus valores. Este contraexemplo cria três funções que consultam o mesmo nome fator:

funcoes = [lambda x: fator * x for fator in range(1, 4)]
print([f(10) for f in funcoes])

A saída é [30, 30, 30]. Quando chamamos as funções, o percurso já acabou e fator vale 3. Com [multiplicador(fator) for fator in range(1, 4)], cada chamada cria um contexto próprio e a saída passa a [10, 20, 30].

Compor sem perder o contrato

Para compor f e g, calculamos primeiro g(x) e passamos esse resultado a f, e a função devolvida conserva ambas:

def compor(f, g):
    return lambda x: f(g(x))

quadrado_depois = compor(lambda x: x * x, lambda x: x + 1)
incremento_depois = compor(lambda x: x + 1, lambda x: x * x)
print(quadrado_depois(3), incremento_depois(3))

A saída é 16 10: (3+1)2(3+1)^2 difere de 32+13^2+1, por isso o resultado de g tem de ser uma entrada válida para f.

Transformar, filtrar e acumular são decisões diferentes. A ordem pode mudar o resultado: elevar ao quadrado antes de filtrar os positivos conserva quadrados de valores originalmente negativos. Escreve primeiro a condição sobre os dados de entrada, depois escolhe a sequência de operações. Uma função chamada por map ou filter também deve respeitar a ausência de efeitos quando esse é o contrato do programa.

Exercícios

Passar a função ou o seu resultado

Prevê o resultado da primeira chamada e explica a falha da segunda. Não alteres as definições; corrige apenas o argumento da chamada que falha.

def f(x):
    return x + 3

def aplicar_duas_vezes(funcao, x):
    return funcao(funcao(x))

print(aplicar_duas_vezes(f, 2))
print(aplicar_duas_vezes(f(2), 2))

Solução

A primeira chamada calcula f(f(2)), isto é, f(5), e escreve 8. Na segunda, Python calcula f(2) antes de entrar em aplicar_duas_vezes, passando o inteiro 5 como funcao. Ao tentar executar funcao(x), lança TypeError: esse inteiro não é chamável. Passar f em vez de f(2) entrega o objeto função e corrige a chamada.

Confere a tua resposta:

  • Obtive 8 na primeira chamada.
  • Identifiquei o inteiro 5 na segunda chamada.
  • Expliquei o TypeError ao tentar chamar 5.
Voltar à explicação
Ordenar com um desempate

Define classificacao(alunos): recebe uma lista de pares (nome, nota) e devolve uma lista por nota decrescente e, nos empates, nome crescente. Conserva a entrada. Procura uma key que represente as duas regras; os testes verificam o comportamento.

Python
def classificacao(alunos):
    return alunos
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Desempatar pelo nome
    Código do teste
    print(classificacao([("B", 10), ("C", 12), ("A", 12)]))
    Esperado:
    [('A', 12), ('C', 12), ('B', 10)]
  2. Lista vazia
    Código do teste
    print(classificacao([]))
    Esperado:
    []
  3. Notas diferentes
    Código do teste
    print(classificacao([("A", 0), ("B", 20), ("C", 10)]))
    Esperado:
    [('B', 20), ('C', 10), ('A', 0)]
  4. Entrada preservada
    Código do teste
    xs = [("B", 10), ("A", 12)]
    print(classificacao(xs))
    print(xs)
    Esperado:
    [('A', 12), ('B', 10)]
    [('B', 10), ('A', 12)]

Pista

Uma chave em tuplo compara o primeiro campo e usa o segundo para desempatar.

Mais uma pista

Nega só a nota. O nome deve continuar crescente.

Solução

O negativo da nota coloca notas maiores primeiro. O nome conserva a ordem crescente. sorted devolve outra lista, sem alterar alunos.

def classificacao(alunos):
    return sorted(alunos, key=lambda aluno: (-aluno[1], aluno[0]))

Erros frequentes

Usar reverse=True na chave completa e inverter também os nomes, ou usar sort e alterar a entrada.

Voltar à explicação
Comparar a ordem de seleção e transformação

Com valores = [-2, 1, 3], calcula a soma dos quadrados depois de filtrar os valores positivos. Compara com elevar todos ao quadrado e só depois filtrar os resultados positivos. Segue o acumulador de reduce, com inicial 0. Esta mudança de ordem também muda os elementos aceites se o filtro selecionar ímpares?

Pista
O quadrado perde o sinal, mas conserva a paridade de um inteiro.
Solução

Ao filtrar positivos primeiro, aceitamos [1, 3], obtemos [1, 9] e o acumulador passa por 0, 1, 10. Ao transformar primeiro, obtemos [4, 1, 9]: os três quadrados são positivos e o acumulador passa por 0, 4, 5, 14. O valor -2, originalmente rejeitado, passou a ter quadrado positivo.

Para inteiros, x é ímpar exatamente quando x * x é ímpar. O filtro de paridade aceita os mesmos elementos nas duas ordens; neste exemplo, rejeita -2 e conserva 1 e 3. Não podemos transferir o contraexemplo do sinal para a paridade.

Confere a tua resposta:

  • Obtive 10 ao filtrar positivos antes dos quadrados.
  • Obtive 14 ao filtrar quadrados positivos.
  • Distingui seleção por sinal de seleção por paridade.
Voltar à explicação
Conservar a ordem do fold

Quanto vale reduce(lambda a, x: a - x, [2, 4], 20)?

Pista
O acumulador é combinado da esquerda para a direita.
Mais uma pista
A primeira combinação é 20 - 2.
Solução
(20 - 2) - 4 = 14. A operação de subtração não permite reagrupar os termos livremente.

Erros frequentes

Calcular 20 - (2 - 4), que daria 22.
Voltar à explicação
Distinguir contextos de funções

Prevê a saída e explica por que não é [10, 20, 30]. Usa a função multiplicador(fator) da lição para obter esse resultado, sem alterar a chamada final.

funcoes = [lambda x: fator * x for fator in range(1, 4)]
print([f(10) for f in funcoes])

Solução

A saída é [30, 30, 30]: as lambdas consultam o mesmo nome fator, cujo valor final é 3. A criação de uma função não congela automaticamente esse valor.

Com funcoes = [multiplicador(fator) for fator in range(1, 4)], cada chamada a multiplicador cria o seu próprio parâmetro local. As funções devolvidas conservam 1, 2 e 3 em contextos diferentes. A chamada final produz [10, 20, 30].

Confere a tua resposta:

  • Previ [30, 30, 30] na primeira versão.
  • Usei multiplicador para criar contextos diferentes.
  • Obtive [10, 20, 30] depois da correção.
Voltar à explicação
Seguir o acumulador

Define soma_quadrados_impares(valores): recebe uma lista de inteiros e devolve a soma dos quadrados dos ímpares, incluindo os negativos. A soma vazia é zero. Usa filter, map e reduce, sem alterar a entrada. Para [1, 2, 3, 4, 5], indica também os estados do acumulador.

Python
from functools import reduce
def soma_quadrados_impares(valores):
    return 0
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Exemplo
    Código do teste
    print(soma_quadrados_impares([1, 2, 3, 4, 5]))
    Esperado:
    35
  2. Vazia
    Código do teste
    print(soma_quadrados_impares([]))
    Esperado:
    0
  3. Só pares
    Código do teste
    print(soma_quadrados_impares([2, 4]))
    Esperado:
    0
  4. Ímpares negativos
    Código do teste
    print(soma_quadrados_impares([-3, -2, 1]))
    Esperado:
    10

Pista

Filtra ímpares, eleva ao quadrado e acumula de zero.

Mais uma pista

Sem valor inicial, reduce falha na lista vazia; com inicial 0, a entrada vazia devolve 0. Um inteiro negativo também pode ser ímpar: -3 % 2 vale 1.

Solução

O filtro dá 1, 3, 5; os quadrados dão 1, 9, 25; o acumulador passa por 1, 10 e 35.

from functools import reduce

def soma_quadrados_impares(valores):
    impares = filter(lambda x: x % 2 != 0, valores)
    quadrados = map(lambda x: x * x, impares)
    return reduce(lambda acumulado, x: acumulado + x, quadrados, 0)

Erros frequentes

Omitir o valor inicial e falhar na lista vazia; usar x % 2 == 1 and x > 0 e perder os ímpares negativos. Elevar ao quadrado conserva a paridade de inteiros, por isso trocar essas duas etapas não é, por si só, um erro neste problema.

Voltar à explicação
Distinguir cada peça de uma caixa com tudo

Define ha_pecas(pedidas, caixas): pedidas é uma lista de strings e caixas uma lista de conjuntos de strings. Devolve True se cada peça pedida existe em pelo menos uma caixa, podendo usar caixas diferentes. Só verificas a existência, sem consumir quantidades: pedidos repetidos não retiram peças. Se não há pedidos, devolve True. Usa any e all, sem alterar as entradas, e explica por que trocar os percursos muda o problema.

Python
def ha_pecas(pedidas, caixas):
    return False
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Peças distribuídas por caixas
    Código do teste
    print(ha_pecas(["a", "b"], [{"a"}, {"b"}]))
    Esperado:
    True
  2. Uma peça ausente
    Código do teste
    print(ha_pecas(["a", "c"], [{"a", "b"}, {"a"}]))
    Esperado:
    False
  3. Sem pedidos nem caixas
    Código do teste
    print(ha_pecas([], []))
    Esperado:
    True
  4. Pedido com caixas vazias
    Código do teste
    print(ha_pecas(["a"], [set()]), ha_pecas(["a"], []))
    Esperado:
    False False
  5. Reutilizar as caixas para o pedido seguinte
    Código do teste
    print(ha_pecas(["b", "a", "b"], [{"a"}, {"b"}]))
    Esperado:
    True

Solução

Para cada peça, any procura uma caixa que a contenha. all exige que essa procura tenha sucesso para todas as peças.

def ha_pecas(pedidas, caixas):
    return all(any(peca in caixa for caixa in caixas) for peca in pedidas)

Com ['a', 'b'] e [{'a'}, {'b'}], as duas procuras têm sucesso em caixas diferentes. any(all(...)) exigiria uma só caixa com as duas peças e responderia False. Sem pedidos, all recebe um percurso vazio e responde True; com pedidos e sem caixas, a procura interior dá False. Cada peça inicia um percurso novo sobre a lista de caixas. Se reutilizasses um único iterador que já chegou à segunda caixa para encontrar "b", poderias perder a caixa com "a" no pedido seguinte.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.