A programação funcional é um paradigma que trata a computação como a avaliação de funções matemáticas e evita mudanças de estado e dados mutáveis. Em Python, embora não seja uma linguagem puramente funcional, podemos aplicar muitos de seus princípios para escrever código mais previsível, testável e conciso. Nesta aula, vamos explorar os conceitos centrais desse paradigma e como utilizá-los no dia a dia.

Dominar a programação funcional em Python não apenas melhora a qualidade do seu código, mas também o prepara para entender bibliotecas e frameworks que adotam esses conceitos, como o Pandas e o PySpark. Além disso, muitas das funções que você já usa, como map e filter, são heranças desse paradigma.

map, filter, reduce

As funções map, filter e reduce são pilares da programação funcional. Elas permitem operar sobre coleções de dados de forma declarativa, sem a necessidade de loops explícitos e mutação de estado.

map aplica uma função a cada item de um iterável, retornando um novo iterável com os resultados. É uma forma elegante de transformar dados. Por exemplo, se quisermos elevar ao quadrado cada número de uma lista, podemos usar map com uma função lambda.

numeros = [1, 2, 3, 4]
quadrados = list(map(lambda x: x**2, numeros))
print(quadrados)  # [1, 4, 9, 16]

filter seleciona os elementos de um iterável que satisfazem uma condição (função que retorna True/False). Ele retorna um iterável apenas com os elementos que passam no teste. Por exemplo, para filtrar números pares:

numeros = [1, 2, 3, 4, 5, 6]
pares = list(filter(lambda x: x % 2 == 0, numeros))
print(pares)  # [2, 4, 6]

reduce é um pouco diferente: ela acumula os elementos de um iterável em um único valor, aplicando uma função de dois argumentos cumulativamente. Por exemplo, para somar todos os números de uma lista:

from functools import reduce

numeros = [1, 2, 3, 4]
soma = reduce(lambda a, b: a + b, numeros)
print(soma)  # 10

Note que reduce não é uma função embutida em Python 3, ela está no módulo functools. Isso porque, na maioria dos casos, usar um loop ou a função sum() é mais legível. No entanto, reduce é útil para operações mais complexas de acumulação.

functools

O módulo functools fornece funções de ordem superior que interagem com outras funções. As mais conhecidas são reduce, partial, lru_cache e wraps.

partial permite fixar alguns argumentos de uma função, criando uma nova função com menos parâmetros. Isso é útil para reutilizar uma função com configurações pré-definidas. Por exemplo, podemos criar uma função que sempre multiplica por 2:

from functools import partial

def multiplicar(a, b):
    return a * b

dobro = partial(multiplicar, 2)
print(dobro(5))  # 10

lru_cache é um decorador que armazena em cache os resultados de uma função, evitando recalcular valores para os mesmos argumentos. Isso é especialmente útil em funções recursivas ou computacionalmente caras. Exemplo com a sequência de Fibonacci:

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

print(fib(30))  # 832040 (rápido graças ao cache)

wraps é usado ao criar decoradores para preservar os metadados da função original (como nome e docstring). Sem ele, a função decorada perde essas informações. Exemplo:

from functools import wraps

def meu_decorador(funcao):
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        print("Antes")
        resultado = funcao(*args, **kwargs)
        print("Depois")
        return resultado
    return wrapper

@meu_decorador
def ola():
    """Diz olá."""
    print("Olá!")

print(ola.__name__)  # ola
print(ola.__doc__)   # Diz olá.

Imutabilidade

Imutabilidade significa que, uma vez criado, um objeto não pode ser modificado. Em programação funcional, isso é crucial, pois evita efeitos colaterais e torna o código mais previsível. Em Python, alguns tipos são imutáveis por natureza: números, strings, tuplas, frozensets. Já listas, dicionários e conjuntos são mutáveis.

Ao adotar a imutabilidade, você pode compartilhar dados com segurança entre funções sem medo de que sejam alterados inesperadamente. Por exemplo, se você passa uma lista para uma função e ela a modifica, isso pode causar bugs difíceis de rastrear. Em vez disso, prefira criar novas estruturas de dados quando precisar de alterações.

# Mutável (mau exemplo em programação funcional)
def adicionar_item(lista, item):
    lista.append(item)
    return lista

minha_lista = [1, 2]
print(adicionar_item(minha_lista, 3))  # [1, 2, 3]
print(minha_lista)  # [1, 2, 3] (modificada!)

# Imutável (bom exemplo)
def adicionar_item_imutavel(lista, item):
    nova_lista = lista + [item]
    return nova_lista

minha_lista = [1, 2]
print(adicionar_item_imutavel(minha_lista, 3))  # [1, 2, 3]
print(minha_lista)  # [1, 2] (original intacta)

No segundo exemplo, a função não altera a lista original, mas cria uma nova. Isso é mais seguro e alinhado com os princípios funcionais.

Funções puras

Uma função pura é aquela que, dado o mesmo conjunto de entradas, sempre retorna o mesmo resultado e não causa efeitos colaterais observáveis (como modificar variáveis globais, imprimir, ou alterar argumentos). Funções puras são fáceis de testar, raciocinar e paralelizar.

Por exemplo, a função soma abaixo é pura:

def soma(a, b):
    return a + b

Já a função contador não é pura, pois depende de um estado externo mutável:

contador_global = 0

def incrementa():
    global contador_global
    contador_global += 1
    return contador_global

Para tornar o código mais funcional, devemos evitar variáveis globais e modificar argumentos. Em vez disso, podemos retornar novos valores. Por exemplo, para simular um contador, poderíamos usar uma função que retorna uma tupla com o novo valor:

def incrementa(valor):
    return valor + 1

Assim, o estado é passado explicitamente, tornando a função pura.

Boas práticas e observações finais

Ao adotar a programação funcional em Python, tenha em mente que nem sempre é a melhor escolha. Python é uma linguagem multiparadigma, e o estilo imperativo muitas vezes é mais legível. Use os conceitos funcionais quando eles simplificarem o código, como em transformações de dados com map, filter e reduce.

Além disso, prefira funções puras sempre que possível, pois elas facilitam testes e depuração. A imutabilidade, embora nem sempre prática, pode ser aplicada em partes críticas do sistema. Lembre-se de que functools oferece ferramentas poderosas, mas use-as com moderação para não obscurecer a lógica.

Referências

Exercícios

  1. Use map para converter uma lista de temperaturas em Celsius para Fahrenheit. Dada uma lista celsius = [0, 20, 37, 100], crie uma nova lista com as temperaturas em Fahrenheit usando map. A fórmula é F = C * 9/5 + 32.

    ✓ Resposta:
    celsius = [0, 20, 37, 100]
    fahrenheit = list(map(lambda c: c * 9/5 + 32, celsius))
    print(fahrenheit)  # [32.0, 68.0, 98.6, 212.0]
  2. Filtre os números primos de uma lista. Dada uma lista numeros = list(range(2, 30)), use filter para obter apenas os números primos. Você pode criar uma função auxiliar para testar a primalidade.

    ✓ Resposta:
    def eh_primo(n):
        if n < 2:
            return False
        for i in range(2, int(n**0.5) + 1):
            if n % i == 0:
                return False
        return True
    
    numeros = list(range(2, 30))
    primos = list(filter(eh_primo, numeros))
    print(primos)  # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
  3. Use reduce para calcular o fatorial de um número. Dado um número n, calcule n! usando reduce e range.

    ✓ Resposta:
    from functools import reduce
    
    def fatorial(n):
        return reduce(lambda a, b: a * b, range(1, n+1), 1)
    
    print(fatorial(5))  # 120
  4. Crie uma função parcial com partial. Defina uma função potencia(base, expoente) e use partial para criar uma função quadrado que calcula o quadrado de um número.

    ✓ Resposta:
    from functools import partial
    
    def potencia(base, expoente):
        return base ** expoente
    
    quadrado = partial(potencia, expoente=2)
    print(quadrado(5))  # 25
  5. Implemente uma função pura para calcular a média de uma lista. A função deve receber uma lista e retornar a média, sem modificar a lista e sem usar variáveis globais.

    ✓ Resposta:
    def media(lista):
        if not lista:
            return 0
        return sum(lista) / len(lista)
    
    print(media([1, 2, 3, 4]))  # 2.5