Expressões Lambda em Python? WTF?
Vamos aprender mais um recurso novo de programação em Python!
Imagine que você seja uma estrutura de dados em Python e se depare com uma função. Sendo um conjunto de zeros e uns bastante amigável, você decide bater papo com ela:
-Qual seu nome?
-Então... eu não tenho um nome.
-Como assim? Como eu vou saber o que você é?
-É que eu sou uma função anônima. Pode-se dizer que sou uma expressão lambda.
Voltando ao mundo real agora, vamos entender algumas coisas: quando um argumento de função ou uma variável Python recebe uma definição de função muito particular, ela passa a ter comportamento de função, inclusive recebendo parâmetros e fazendo cálculos. Isso é uma função ou expressão lambda.
Ao longo desse texto, irei me referir a essa estrutura de várias formas: expressão lambda, função lambda, função anônima, etc.
Senta que lá vem a História...
tã tã rã rã rã tã tã rã rã rã
Sua origem tem a ver com o Cálculo Lambda (CL), um sistema formal que é a base para linguagens de programação funcionais como Haskell e Lisp. Algo interessante é que ela é equivalente, em poder computacional, a uma Máquina de Turing, sendo capaz de realizar todas as operações que um computador comum consegue. Sempre que uma linguagem consegue isso, dizemos que ela é Turing completa.
Entretanto, da mesma forma que não seria muito prático fazer algoritmos do dia a dia usando Máquinas de Turing, também não é cômodo usar o CL para expressar computação no cotidiano. Entretanto, a notação do CL em que funções simplificadas são aplicadas e compostas ao longo do código pode ser bem conveniente em linguagens de programação interpretadas, de alto nível e propósito geral, caso de Python.
Chega de passado, mostra logo isso daí!
Expressões ou funções lambda acabam sendo uma forma prática de se criar uma função que realiza alguma tarefa bastante simplificada e bem localizada. Elas são úteis quando não se justifica, por exemplo, criar uma função global para o projeto em questão.
Uma definição básica de expressão lambda em Python pode ser vista abaixo.
#variavel = lambda [lista de parâmetros]: expressão
f = lambda x: x**2Ao usar a palavra-chave lambda, estamos dizendo que a variável f acima irá receber uma expressão lambda ou função anônima. No caso, o parâmetro dela é x e a expressão é como se fosse o corpo da função que criamos. No caso, o x (seja lá o que ele for) será elevado ao quadrado (x**2).
Vamos ver um exemplo mais completo para entender.
#expressão lambda criada
expLambdaAoQuadrado = lambda x: x**2
#função que realiza a mesma coisa que a exp. lambda acima
def computaAoQuadrado(x):
return x**2
if __name__ == "__main__":
#print testando ambas as funções
print("6 ao quadrado usando a função comum:", computaAoQuadrado(6))
print("6 ao quadrado usando a função anônima:", expLambdaAoQuadrado(6))Criamos uma função convencional (linha 5) e uma expressão lambda (linha 2) que fazem exatamente a mesma coisa: pegam um valor x e computam seu quadrado. Ambas as expressões chegam no mesmo resultado, como podemos ver no print abaixo.
As duas funções computam o quadrado do número x = 6.
No caso, a expressão lambda x: x**2 só possui um único parâmetro, que é x. E se precisarmos de mais argumentos?
E se eu quiser adicionar 2 ou mais parâmetros?
Nesse caso é só introduzir uma vírgula para cada parâmetro.
if __name__ == "__main__":
#duas expressões lambda que somam 2 e 3 números
soma2Numeros = lambda x, y: x+y
soma3Numeros = lambda x, y, z: x+y+z
#apresento as somas na tela
print("Soma de 2 + 3 =",soma2Numeros(2, 3))
print("Soma de 2 + 3 + 5 =",soma3Numeros(2, 3, 5))Veja no código acima dessa janela de console que criamos duas funções lambda dentro do “main”. A primeira soma dois valores x, y (linha 3) e a segunda soma três valores x, y, z (linha 4).
Esse exemplo mostra como é cômodo criar uma função anônima que seja muito específica ao escopo atual. Se as funções lambda soma2Numeros e soma3Numeros só forem usadas naquele contexto, então posso definí-las diretamente onde serão aplicadas.
Expressões lambda com parâmetros padrão
Algo interessante das funções lambda é que elas podem ter um argumento padrão, se nenhum for informado ao chamá-la. Veja o exemplo abaixo, em que criamos uma função de soma.
if __name__ == "__main__":
soma = lambda x= 0, y= 0: x + y
print(soma(6, 5))
print(soma(6))
print(soma())As três execuções do código acima estão apresentadas nessa imagem.
Na linha 2 eu defini dois argumentos x e y, só que fiz ambos receberem 0 na definição da função (x = 0 e y = 0). Assim, quando um desses argumentos não for usado, por definição o argumento assumirá o valor 0.
Observe que, na linha 5, o x assume o valor 6 enquanto o y, por não receber nada, receberá 0. Ao final, a soma dá 6.
Até mesmo invocar a função lambda soma() sem nada é possível nesse caso, com x e y recebendo 0 e, ao final, a saída resultando em 0 (linha 6).
Precisa ser apenas números?
Não! Como as expressões lambda são funções, elas podem receber qualquer tipo de dados válido como parâmetro. Strings, por exemplo. Veja o código abaixo, que recebe um nome completo e o retorna com apenas a primeira letra de cada segmento em maiúsculas.
if __name__ == "__main__":
meuNomeCompleto = lambda primeiro, segundo, ultimo: "Nome Completo: " + primeiro.title() + " " + segundo.title() + " " + ultimo.title()
print(meuNomeCompleto("lucas", "grassano", "lattari"))
print(meuNomeCompleto("LuCaS", "GrAsSaNo", "LATTARI"))Na linha 2 temos a função lambda recebendo 3 argumentos: primeiro, segundo e ultimo. Cada parâmetro é uma parte do nome total. A definição dos argumentos encontra-se definida no trecho lambda primeiro, segundo, ultimo:
O corpo dessa função lambda, por sua vez, é simplesmente a geração e o retorno da string "Nome Completo: " + primeiro.title() + " " + segundo.title() + " " + ultimo.title(), que forma o nome completo. Lembrando que a função title() transforma cada palavra em um título (primeira letra em maiúsculas, e o restante não).
Com isso, as saídas para as chamadas das linhas 4 e 5, respectivamente, encontram-se abaixo.
Veja como os dois textos gerados ficaram exatamente iguais.
No entanto, a coisa pode piorar muito: por ser possível criar uma variável que “guarda” uma função, você sabia que é possível passar uma expressão lambda como parâmetro para outra função?
Como assim? Expressão lambda como parâmetro de outra função?
Pois é. Considere, por exemplo, o código abaixo.
def ao_cubo(x, fn):
return fn(x) ** 3
if __name__ == "__main__":
print(ao_cubo(2, lambda x: x ** 2))O que ocorreu aqui???
Vamos entender o que houve. Basicamente, na linha 5, passamos dois parâmetros para a função ao_cubo(). O primeiro foi o 2 e o segundo foi a expressão lambda que eleva x ao quadrado.
Já no interior da função ao_cubo() (linha 2), a primeira coisa que é feita é computar fn(x), que é pegar o x (atualmente valendo 2) e aplicá-lo a lambda x: x ** 2, que é o fn em questão, passado como argumento de ao_cubo (linha 1). Nisso, o 2 é elevado ao quadrado, gerando 4.
A seguir, o 4 é elevado ao cubo (linha 2), gerando 64 e retornando isso ao print colocado no "main".
Computando a Média com Funções Lambda
Usando as funcionalidades aprendidas até aqui, podemos criar uma função lambda que computa a média de estudantes sem demandar mais do que uma única linha. Quando conseguimos fazer muitas operações com poucas linhas, aí estamos aproveitando o potencial da linguagem e tornando nossa escrita cada vez mais "pythônica".
if __name__ == "__main__":
computaMedia = lambda *args: sum(args) / len(args)
print("Media 1:",computaMedia(18))
print("Media 2:",computaMedia(18, 14, 16))
print("Media 3:",computaMedia(18, 17, 21, 16, 14, 35))Médias calculadas do código acima.
Na linha 2 temos a definição da função lambda que computa a média. O parâmetro *args é uma sintaxe comumente usada quando criamos um procedimento que pode ter um número arbitrário de parâmetros.
O corpo da função lambda, por sua vez, é sum(args) / len(args). A função sum() soma a tupla args, que contém todos os números passados por parâmetro. A função len(), por sua vez, calcula o número de elementos da tupla.
Observe que nas chamadas da função lambda definida em computaMedia temos diferentes números de parâmetros: um só (linha 3), três (linha 4) e 6 (linha 5). Na linha 4, por exemplo, a operação feita será a soma 18 + 14 + 16 que, dividida por 3, resultará em 16,0.
Só cuidado pois, da forma como está, se nenhum valor for passado para computaMedia(), haverá uma divisão por zero que ocasionará erro =)
Lambda e Ordenação
Um uso bem interessante para expressões Lambda é combiná-las com a função de ordenação sort() ou sorted(), mais especificamente em seu parâmetro key.
Caso você não saiba, a função sort() ordena uma lista de elementos, seja numericamente ou segundo a ordem alfabética. Um parâmetro opcional dessa função é justamente key, que especifica uma outra função (que pode ser uma expressão lambda) a ser chamada para cada elemento antes de ser realizada a comparação que definirá sua nova posição. Assim, sort() modifica a lista atual, colocando-a em ordem.
Quanto a sorted(), ela funciona para qualquer iterável além da própria lista, produzindo uma nova variável com o conjunto ordenado. Assim, sorted() não modifica a estrutura atual.
Por exemplo, suponha que você tenha tuplas de dados de alguns estudantes e desejamos ordená-los por cada campo separadamente. Podemos fazer isso conforme o exemplo abaixo.
if __name__ == "__main__":
alunos_tuplas = [
('Lucas', 200422033, 'Computação'),
('Eduardo', 200566044, 'Arquitetura'),
('Carla', 200855022, 'Física Nuclear')
]
#ordenando pelo primeiro campo, que é o nome
print(sorted(alunos_tuplas))
#ordenando pela matrícula, que é o próximo campo
print(sorted(alunos_tuplas, key = lambda x: x[1]))
#ordenando pelo curso, que é o último campo
print(sorted(alunos_tuplas, key = lambda x: x[2]))Veja como cada linha de código produziu uma ordenação diferente.
Observe como a ordenação pode ser feita usando campos diferentes do conjunto de dados por meio das expressões lambda.
Na linha 9 do código acima, a ordenação é feita em cima de cada nome, como Carla, Eduardo e Lucas. Isso ocorre pois, no caso de tuplas, por padrão o primeiro campo (nome) é a chave para a ordenação. Assim, é respeitada a ordem alfabética segundo os nomes.
Já na linha 10, a chave passada foi a expressão lambda x: x[1], o que faz com que o campo de matrícula seja usado, já que ele é o segundo da tupla ([0] é o primeiro). Assim, a ordem ficou: Lucas, Eduardo e Carla, como é possível ver no print acima.
Finalmente, na linha 11, a chave passada foi o curso, que é o terceiro (e último) campo. Assim, como Arquitetura vem primeiro, seguido de Computação e Física Nuclear, sua ordem ficou: Eduardo, Lucas e Carla.
Assim, podemos ter mais flexibilidade ao ordenar tipos de dados diversos usando expressões lambda.
Inclusive, na seção a seguir, mostro uma forma de chegar em algo parecido usando Orientação a Objetos.
Ordenação com Orientação a Objetos e Lambda
Também é possível adaptar a lógica anterior para classes e instâncias. Veja o código abaixo.
class Aluno:
def __init__(self, nome, matricula, curso):
self.nome = nome
self.matricula = matricula
self.curso = curso
def __repr__(self):
return repr((self.nome, self.matricula, self.curso))
if __name__ == "__main__":
estudantes_lista = [
Aluno('Lucas', 200422033, 'Computação'),
Aluno('Eduardo', 200566044, 'Arquitetura'),
Aluno('Carla', 200855022, 'Física Nuclear')
]
#ordenando por nome
print(sorted(estudantes_lista, key = lambda x: x.nome))
#ordenando por matrícula
print(sorted(estudantes_lista, key = lambda x: x.matricula))
#ordenando por nome de curso
print(sorted(estudantes_lista, key = lambda x: x.curso))Veja como o resultado da ordenação foi bem semelhante ao exemplo anterior.
Na linha 1 definimos a classe Aluno, que terá os atributos nome, matrícula e curso (linhas 3, 4 e 5). É importante dizer que na Orientação a Objetos em Python, os atributos da classe são definidos no interior do construtor __init__().
Outro ponto que chama a atenção é o __repr__() usado na linha 7. Ele é chamado sempre que for preciso representar aquela instância como uma string. É semelhante ao toString() do Javascript ou Java. Usamos isso para "imprimir" na tela cada objeto, a fim de sabemos se a ordenação foi feita corretamente. Num futuro (que eu espero que não esteja muito distante) eu farei um tutorial com o essencial de Orientação a Objetos em Python.
De resto, a lógica do uso das expressões lambda é muito similar ao exemplo anterior. Na linha 11, crio uma lista contendo 3 objetos alunos, para representar Lucas, Eduardo e Carla.
Na linha 17, por exemplo, eu defino que a ordenação será feita em cima do nome, pois esse que é o elemento usado e retornado para a ordenação, no parâmetro key (lambda x: x.nome, em que x é o objeto aluno).
O interessante dessa abordagem é que ela é bastante legível e bem pythônica!
Mapeando Elementos para Outra Função
Um uso interessante das expressões lambda é em conjunto com a função map(). Essa função aplica uma operação (outra função, que pode ser lambda) em cada item de uma lista, vetor ou outro iterável. Veja o exemplo abaixo.
if __name__ == "__main__":
canais = ["peixe babel", "programação dinâmica", "filho da nuvem"]
total_de_caracteres = map(lambda x: len(x), canais)
print(list(total_de_caracteres))
total_de_caracteres_sem_espaco = map(lambda x: len(x.replace(" ", "")), canais)
print(list(total_de_caracteres_sem_espaco))Print contendo o total de caracteres de cada canal, com e sem espaços.
Na linha 2, criei uma lista com nomes de canais do YouTube de tecnologia e programação. A seguir, na linha 4, chamei a função map() para criar uma nova lista com o total de caracteres de cada canal. A função map() constrói uma nova lista em que, cada elemento gerado é a aplicação da função len(), que conta o total de letras que cada nome tem. O len() é aplicado em cada x (cada canal) por intermédio do lambda.
O print (linha 5) apresenta a lista com os valores 11, 20 e 14. O 11 é o total de caracteres de “peixe babel”, o 20 é o número de caracteres de “programação dinâmica” e assim por diante. Note que eu precisei usar a função list() nessa mesma linha para transformar o objeto map produzido na linha 4 em lista, para que ele pudesse ser imprimível na tela.
Veja que é possível fazer algo ainda mais rebuscado na linha 7, em que o lambda foi usado não apenas para contar o total de caracteres, mas também para remover os espaços vazios do nome de cada canal antes da contagem. Isso é feito com x.replace(” “, “”), que substitui o espaço vazio do primeiro parâmetro pela ausência de espaços do segundo parâmetro em .
Convém dizer que não é obrigatório usar uma expressão lambda como parâmetro da função map(). Poderia ser usada uma função convencional, o que não é tão prático quanto o lambda. O código abaixo possui o mesmo efeito do trecho acima.
def contaLetras(lista):
return len(lista)
def contaLetrasSemEspaco(lista):
return len(lista.replace(" ", ""))
if __name__ == "__main__":
canais = ["peixe babel", "programação dinâmica", "filho da nuvem"]
total_de_caracteres = map(contaLetras, canais)
print(list(total_de_caracteres))
total_de_caracteres_sem_espaco = map(contaLetrasSemEspaco, canais)
print(list(total_de_caracteres_sem_espaco))Filtragem de Elementos
Suponha que você tenha uma lista de elementos e queira selecionar os que atendem um certo critério, por exemplo, obter apenas os números pares de uma lista de inteiros. Existe uma função bem apropriada para isso, chamada filter() e ela tem esse nome por "filtrar" os elementos que atendem a uma certa condição, que é definida por uma função (seja ela lambda ou não).
O código abaixo usa o método filter() e o lambda para "deixar passar" apenas os elementos pares, filtrando-os.
if __name__ == "__main__":
lista_de_0_a_99 = list(range(0, 100))
lista_com_pares = list(filter(lambda x: x % 2 == 0, lista_de_0_a_99))
print(lista_com_pares)Print apresentando apenas os números pares da lista de 0 a 99.
Na linha 2, criamos uma lista contendo números inteiros de 0 a 99. A função range(0, 100) produz todo o intervalo de números ([0, 1, 2, 3, …, 99]) e a função list() a transforma em um tipo de dados lista.
Já na linha 3, para “filtrar” apenas os pares, usamos a combinação filter(lambda x: x % 2 == 0, lista_de_0_a_99), em que filter() recebe, como primeiro parâmetro, a função lambda que define a condição a ser atendida (no caso, se x % 2 == 0, ou seja, se x divisível por 2, então ele é “filtrado” para a nova lista) e, como segundo parâmetro, a lista original.
Com isso, a estrutura lista_com_pares recebe apenas os valores "filtrados", cuja condição x % 2 == 0 é verdadeira.
Redução de Elementos
Outra técnica de programação funcional bem conhecida implementada em Python é a redução, que consiste em aplicar uma operação em pares de elementos, reduzindo-os a um único resultado. Isso é feito nos dois primeiros elementos do conjunto, que pode ser uma lista, por exemplo. A seguir, esse resultado é aplicado ao resultado obtido pros dois primeiros elementos junto ao terceiro elemento, e assim por diante. Isso é feito até que reste um único elemento.
Veja um exemplo para entender melhor. Considere que você queira somar todos os elementos de uma lista, como [2, 4, 7, 8]. Por meio da redução, você iniciaria somando 2 + 4 = 6. Após, esse 6 é somado com 7, gerando 13. Finalmente, 13 + 8 = 21. Assim, o conjunto original foi reduzido ao 21.
A redução é feita por meio de uma invocação a reduce() e é a aplicação recursiva de uma operação definida em cima de uma função, que pode ser atribuída com o operador lambda. Implementemos em Python a soma de elementos que exemplificamos acima.
from functools import reduce
if __name__ == "__main__":
lista = [2, 4, 7, 8]
soma = reduce(lambda a, b: a + b, lista)
print(soma)Resultado da soma da lista acima: 21.
Observe que para usar reduce() você deve importar o módulo reduce em functools (linha 1).
Na linha 5, o operador lambda a, b: a + b irá aplicar sucessivamente as somas por meio da chamada de reduce(). Assim, na primeira iteração, será somado 2 e 4, com a = 2 e b = 4. Logo a seguir, o 6 da iteração anterior será somado com 7, tal que a = 6 e b = 7. Isso ocorrerá continuamente até que apenas 21 “sobre” e seja o resultado final da redução.
Fatorial usando Reduce
Se você não estiver lembrado do conceito de número fatorial, saiba que ele é a multiplicação sucessiva de um número por seu antecessor, o antecessor do antecessor, etc. Por exemplo, o fatorial de 4 é 4 x 3 x 2 x 1 = 24. Usamos o símbolo de exclamação para representá-lo: 4! = 24.
Usando o que sabemos da função range() com o que aprendemos do reduce(), podemos implementar o conceito de fatorial de uma forma diferente e sintética.
from functools import reduce
if __name__ == "__main__":
n = 4
sequencia = list(range(1, n + 1))
print(n,"! =", reduce(lambda a, b: a * b, sequencia))Resultado do fatorial de 4, que é 24.
No código acima, computa-se o fatorial de n = 4 (4! = 24). Na linha 5, gera-se uma lista com a sequência de números, começando de 1 até 4. Na linha 6, tem-se a invocação do reduce(), em que o primeiro parâmetro é o lambda que realiza as sucessivas multiplicações (lambda a, b: a * b) e o segundo argumento é a lista com a sequência de inteiros. Nisso, multiplica-se:
a = 1 x b = 2 => 2
a = 2 x b = 3 => 6
a = 6 x b = 4 => 24
Conclusão e Limitações
Operadores Lambda não são obrigatórios para uso, mas são um recurso bem interessante para tornar seu código bem "pythônico", ou em outras palavras, com bastante expressividade (em poucas linhas, fazer muita coisa). Assim, você não precisa necessariamente usá-las quando quer resolver algo, mas sem dúvida pode ser uma forma elegante de lidar com algumas questões no seu código.
A maior limitação das funções lambda são o fato dela ter que possuir uma única expressão ou usar uma única linha. Não dá pra fazer uma função lambda com múltiplas atribuições, comentários nem nada do tipo. É uma operação e pronto! O que, em muitos exemplos apresentados ao longo do texto, costuma ocorrer com frequência.
Dessa forma, espero que esse texto tenha ajudado a compreender as nuances de um dos recursos avançados mais úteis de se saber em Python e que também pode ser aplicados em outras linguagens, como é o caso de C# e Javascript.
Pretendo fazer outros textos falando de recursos mais complexos que sejam úteis de se conhecer e capazes de aumentar sua bagagem de conhecimento enquanto desenvolvedor. Até lá!















