O itertools
é um módulo fantástico da bibliotéca padrão do python, para trabalhar com iteradores e estruturas complexas de dados.
Porém, é recomendado um conhecimento mínimo sobre geradores para evitar possíveis armadilhas.
Sim, eu cai em mais uma armadilha do Python, dessa vez foi o groupby
do
módulo itertools.
O que é o groupby
?
O groupby consiste em uma função que, baseado em um iterável, retorna uma estrutura de agrupamendo com um valor de chave e um grupo de valores, relacionados a essa chave. A função groupby possui a seguinte syntax:
defgroupby(iterable,key=None)
Onde:
- Iterable: Qualquer iterável (e.g. lista, tupla, gerador, dicionário, etc.).
- key: Uma key function que será aplicada em cada elemento do iterável afim de retornar a chave para o agrupamento.
O resultado da função groupbyé um gerador onde cada iteração retorna o valor da chave e outro gerador com os valores que foram agrupados para essa chave, por exemplo:
>>>fromitertoolsimportgroupby>>>items=[('animal','dog'),('animal','cat'),('person','john')]>>>forthing,valuesingroupby(items,key=lambdax:x[0]):...print('{}: {}'.format(thing,list(values)))...animal:[('animal','dog'),('animal','cat')]person:[('person','john')]
Usei o
list()
novalues
para poder resolver o gerador e apresentar os valores no print (não a instancia do gerador).
A armadilha
Como você pode ver, o groupbyé realmente muito útil e poderoso, porém, o que poderia acontecer caso o iterável não estivesse préviamente ordenado pelo mesmo critério a ser utilizado para o agrupamento? Vamos adaptar o exemplo anterior para realizar esse teste:
>>>fromitertoolsimportgroupby>>>items=[('animal','dog'),('person','john'),('animal','cat')]>>>forthing,valuesingroupby(items,key=lambdax:x[0]):...print('{}: {}'.format(thing,list(values)))...animal:[('animal','dog')]person:[('person','john')]animal:[('animal','cat')]
Como você pode ver, o agrupamento falha, retornado a mesma chave mais de uma vez com um grupo de valores distintos.
Por que isso acontece ?
Isso acontece porque internamente, o groupby gera um novo grupo a cada novo valor de chave que for encontrado no iterável. Mesmo que uma chave se repita, o groupby não consegue "olhar para atrás" e verificar os grupos que já foram gerados.
Como resolver?
Simples, basta antes de agrupar, ordenar o iterável pela mesma chave que será utlizada no agrupamento do groupby, por exemplo:
>>>fromitertoolsimportgroupby>>>items=[('animal','dog'),('person','john'),('animal','cat')]>>>ordered_items=sorted(items,key=lambdax:x[0])>>>forthing,valuesingroupby(ordered_items,key=lambdax:x[0]):...print('{}: {}'.format(thing,list(values)))...animal:[('animal','dog'),('animal','cat')]person:[('person','john')]
Como se prevenir?
Simples, leia a documentação!!! Sim, meu vacilo foi ainda maior pois, a documentação oficial do python alerta sobre esse risco:
The operation of groupby() is similar to the uniq filter in Unix. It generates a break or new group every time the value of the key function changes (which is why it is usually necessary to have sorted the data using the same key function). That behavior differs from SQL’s GROUP BY which aggregates common elements regardless of their input order.
Tudo bem que poderia ter um destaque maior esse alerta ou até mesmo um exemplo, porém, não adianta reclamar que não está documentado =).
Referências
Documentação Oficial