Skip to content

Tudo para WordPress, desenvolvimento web — e não só

🐍 Programação funcional em Python

🐍 Programação funcional em Python

Qualquer programador Python, mais cedo ou mais tarde, ouve: «experimenta escrever num estilo funcional, o teu código ficará mais limpo». Mas quando abres a documentação, vês map, filter e reduce com lambdas e não percebes por onde começar.

O problema não é a programação funcional ser difícil. O problema é que a maioria dos guias ou se perde em sebes académicas de Haskell ou se limita à sintaxe sem explicar o «porquê». O Python, sendo uma linguagem multiparadigma, fornece exatamente as ferramentas funcionais de que precisas para as tarefas do dia a dia, sem extremismos.

Neste material, uma análise prática das capacidades funcionais do Python: das compreensões de lista às funções lambda, com exemplos reais e cenários onde cada ferramenta simplifica genuinamente o código.

💡 Visão geral rápida:

  • Decompor compreensões de lista, porque são melhores do que ciclos e quando as usar
  • Dominar geradores: yield, poupança de memória e expressões geradoras
  • Percorrer map + filter + reduce: exemplos práticos de substituição de ciclos
  • Aprender a escrever funções lambda e perceber onde são adequadas e onde prejudicam a legibilidade

Programação funcional, do que estamos a falar

A programação funcional é uma abordagem onde um programa é construído a partir de funções matemáticas: estas recebem dados de entrada imutáveis e devolvem um resultado sem alterar nada externo. Sem efeitos colaterais, sem modificar o estado global.

Em linguagens funcionais puras como Haskell, as partes «puras» e «impuras» (que interagem com o mundo exterior) do programa são estritamente separadas. Esta abordagem permite a prova formal da correção do código: o compilador garante a ausência de efeitos colaterais inesperados.

O Python não foi concebido como uma linguagem funcional. É multiparadigma: orientado a objetos, procedural e, sim, também funcional. Escrever em Python num estilo puramente funcional é como martelar pregos com um microscópio: tecnicamente possível, mas desconfortável e pouco natural. No entanto, pegar em ferramentas funcionais individuais e integrá-las em código familiar é um padrão funcional e útil.

Compreensões de lista, a principal ferramenta funcional do Python

O pão com manteiga da programação funcional é trabalhar com listas. Selecionar elementos por condição, transformar cada elemento, construir uma nova lista a partir de uma existente, tudo isto em Python é coberto pelas compreensões de lista.

Antes, ciclo procedural:

1def filter_odd(li):
2 result = []
3 for i in li:
4 if i % 2 == 1:
5 result.append(i)
6 return result
7
8print(filter_odd([2, 4, 6, 7, 8, 1, 19, 200, 42, 31]))

Depois, compreensão de lista:

1li = [2, 4, 6, 7, 8, 1, 19, 200, 42, 31]
2odd_numbers = [x for x in li if x % 2 == 1]
3print(odd_numbers) # [7, 1, 19, 31]

Quatro linhas de código procedural comprimidas numa só. A legibilidade não sofreu: a sintaxe [expression for element in iterator if condition] é intuitivamente clara, «toma x de li se x for ímpar».

E isto não é apenas açúcar sintático. A compreensão de lista é executada mais rapidamente do que um ciclo equivalente com .append() porque é executada ao nível do C, e não ao nível do interpretador Python.

O que é um iterador

A compreensão de lista baseia-se no conceito de iterador, um objeto que devolve o próximo elemento de uma sequência quando lhe é pedido next(). Qualquer objeto que implemente o método __iter__ é designado iterável. Lista, string, tuplo, dicionário, conjunto, são todos iteráveis.

1spam_iter = iter("foobar")
2result = "".join([c.upper() for c in spam_iter])
3print(result) # FOOBAR

Aqui iter() devolve um iterador de string, e a compreensão de lista itera através dele, aplicando .upper() a cada caractere.

Geradores, sequências preguiçosas

Um iterador pode ser criado não só a partir de uma coleção existente, mas também usando uma função geradora. Em vez de return, usa yield: a função «adormece», lembrando o seu estado, e na próxima chamada de next() continua do mesmo ponto.

1def gen(max_val):
2 i = 1
3 while i < max_val:
4 yield i
5 i += 1
6
7g = gen(1000)
8for _ in range(12):
9 print(next(g))

Este código irá imprimir números de 1 a 12. A função gen não cria uma lista de mil elementos em memória, gera valores um a um, sob demanda. Para sequências de milhões de registos (linhas de um ficheiro de log, stream de base de dados), a diferença no consumo de memória é de ordens de magnitude.

Expressões geradoras

Sintaxe compacta: o mesmo que as compreensões de lista, mas com parênteses em vez de parênteses retos.

1g = (c.upper() for c in "foobar")

Isto não é um tuplo, é um gerador. A regra é simples: parênteses retos → lista (ávida, tudo em memória), parênteses curvos → gerador (preguiçoso, um elemento de cada vez).

Os geradores são uma ponte entre o estilo procedural e o funcional em Python: fornecem avaliação preguiçosa sem mergulhar na teoria das mónadas.

Map, filter e reduce: três pilares do processamento funcional

Três funções integradas que nas linguagens funcionais são a base de tudo. O Python implementa-as à sua maneira, e é importante conhecer as nuances das versões específicas do Python.

Map, aplicar uma função a cada elemento

1def square(x):
2 return x * x
3
4result = map(square, [1, 2, 3])
5print(list(result)) # [1, 4, 9]

map recebe uma função e uma sequência, e devolve um iterador com os resultados da aplicação da função a cada elemento. No Python 3, map devolve um iterador, não uma lista, pelo que precisas de list() para ver o resultado.

Para transformações simples, a compreensão de lista é muitas vezes mais legível:

1[x * x for x in [1, 2, 3]] # same thing, but more familiar

Filter, selecionar elementos por condição

1def is_upper(c):
2 return c == c.upper()
3
4result = filter(is_upper, "FreedominObscureandoutlandishcOde")
5print(list(result)) # ['F', 'O', 'O']

filter mantém apenas os elementos para os quais a função predicado devolve True. Tal como map, devolve um iterador no Python 3.

Reduce, reduzir uma sequência a um único valor

Ao contrário de map e filter, reduce não reside no âmbito integrado, mas no módulo functools. Aplica sequencialmente uma função aos elementos, acumulando o resultado.

1from functools import reduce
2
3def add(a, b):
4 return a + b
5
6print(reduce(add, range(1, 6))) # 15

Nos bastidores: ((((1 + 2) + 3) + 4) + 5) = 15. Para somar números em Python existe a função integrada sum(), pelo que reduce é mais usado para reduções não padronizadas: construir uma árvore a partir de uma lista plana, fundir dicionários aninhados, calcular o máximo divisor comum de uma sequência.

Funções lambda, auxiliares anónimos

Quando uma função é necessária exatamente uma vez e o seu corpo cabe numa expressão, usa lambda:

1lambda arguments: expression

O mesmo exemplo de filter pode ser reescrito sem um def separado:

1result = list(filter(lambda c: c == c.upper(), "FreedominObscureandoutlandishcOde"))

Lambda é um compromisso. Vantagem: não é necessário declarar uma função separada para uma verificação trivial. Desvantagem: a legibilidade diminui se a expressão se tornar mais complexa do que um par de operações. Regra prática: se a lambda não couber numa linha ou começares a aninhar lambdas umas dentro das outras, extrai para um def normal.

Onde as lambdas são realmente úteis

O cenário mais comum é a ordenação por uma chave não padronizada:

1users = [{"name": "Alice", "age": 31}, {"name": "Bob", "age": 25}]
2users.sort(key=lambda u: u["age"])

Ou transformação rápida em map/filter quando o corpo é verdadeiramente trivial:

1squares = list(map(lambda x: x * x, range(10)))

Mas se estiveres a pensar numa lambda para lógica com quatro ou cinco operações, para. Um nome de função funciona como documentação, e def fornece espaço para uma docstring.

⁉️🤔 Perguntas comuns

Quando é que uma compreensão de lista é melhor do que map?

Uma compreensão de lista como [x*2 for x in data] lê-se de forma mais natural do que list(map(lambda x: x*2, data)). Uma operação integrada com uma lambda tem dois níveis de indireção, enquanto uma compreensão de lista tem um. Mas se a função já existir como um def separado, map com ela é conciso. A compreensão de lista ganha em legibilidade para transformações simples, especialmente com filtragem através de [... if ...]. Para lógica complexa com variáveis intermédias, um ciclo for normal é imbatível.

Gerador ou lista, qual escolher?

Se o resultado for necessário várias vezes (iteração, indexação, comprimento), escolhe uma lista. Se iterares uma vez e o volume de dados for grande, um gerador poupará memória. Uma lista de um milhão de inteiros ocupa cerca de 8 MB, um gerador para o mesmo intervalo ocupa menos de um kilobyte. Mas um gerador não pode ser «rebobinado» nem acedido por índice. Regra prática: dados de um ficheiro ou base de dados, gerador; resultado para uso repetido, list().

As lambdas prejudicam a legibilidade do código?

Em pequenas doses e para lógica trivial, não. key=lambda x: x["price"] lê-se instantaneamente. Mas assim que uma lambda deixar de ser «um pensamento», extrai-a para uma função normal. As equipas com revisão de código limitam as lambdas a uma expressão sem aninhamento. Se um colega precisar de mais de cinco segundos para a compreender, reescreve-a como um def. O nome serve como documentação: def by_price(item): return item["price"] é mais claro do que uma lambda anónima.

Quão mais lento é o Python funcional em comparação com o procedural?

Para a maioria das tarefas do dia a dia, a diferença é negligenciável, frações de microssegundo. A compreensão de lista é frequentemente mais rápida do que um ciclo equivalente porque é implementada em C. O gargalo nos programas Python não é a escolha entre map e um ciclo, mas a entrada/saída, os pedidos de rede e a arquitetura em geral. Escreve com clareza, traça os pontos críticos com cProfile e otimiza apenas o que realmente torna as coisas lentas.

O que usar no teu código hoje

As ferramentas funcionais do Python não são dogma, mas alavancas. Não substituem o código procedural e orientado a objetos, mas complementam-no em pontos específicos.

Começa com as compreensões de lista, esta é a aplicação mais comum e segura. Domina os geradores: assim que vires uma lista temporária que é usada uma vez, pergunta-te, não deveria isto ser substituído por yield? Usa map e filter onde eles genuinamente simplificam uma cadeia de transformação. Guarda as lambdas para key= em ordenações e callbacks triviais.

O estilo funcional em Python não tem a ver com «pureza», mas com expressividade. Se depois de substituíres um ciclo por uma compreensão de lista o código ficou mais claro, fizeste tudo bem. Tenta reescrever um método problemático numa abordagem funcional e compara o diff: muitas vezes o resultado fala por si.