Dividindo listas grandes em sub-listas de n elementos (Python)
Overview
Neste tutorial de Python, mergulharemos na arte de dividir uma grande lista em pedaços mais pequenos e gerenciáveis, usando como exemplo a lista do elenco de Avengers: Infinity War. Aprenda de forma descomplicada como a função yield pode tornar esse processo não apenas possível, mas eficiente e elegante.
Em um projeto recente, gerei uma lista de statements que precisavam ser enviados para o banco de dados. Como a lista ficou insanamente grande, pesquisei um modo de quebrar esta mega lisa em pedaços menores e mais gerenciáveis.
O exemplo que utilizarei aqui será uma lista de strings, mas funciona para qualquer lista. Para não criar uma lista genérica com “a”, “b”, “c” como elementos, peguei emprestado do site IMDB os 31 primeiros atores do filme Avengers: Infinity War.
Sendo assim, temos a lista:
avengers = ['Robert Downey Jr.', 'Chris Hemsworth', 'Mark Ruffalo', 'Chris Evans', 'Scarlett Johansson', 'Don Cheadle', 'Benedict Cumberbatch', 'Tom Holland', 'Chadwick Boseman', 'Zoe Saldana', 'Karen Gillan', 'Tom Hiddleston', 'Paul Bettany', 'Elizabeth Olsen', 'Anthony Mackie', 'Sebastian Stan', 'Idris Elba', 'Danai Gurira', 'Peter Dinklage', 'Benedict Wong', 'Pom Klementieff', 'Dave Bautista', 'Vin Diesel', 'Bradley Cooper', 'Gwyneth Paltrow', 'Benicio Del Toro', 'Josh Brolin', 'Chris Pratt', 'Sean Gunn', 'William Hurt', 'Letitia Wright']
Utilizando yield
Para resolver este problema, vamos utilizar o generator e o yield. Veja o exemplo de função abaixo:
def create_sublists(big_list, sublist_size):
qty_el = len(big_list)
for i in range(0, qty_el, sublist_size):
# Create an index range for l of n items:
yield big_list[i:i+sublist_size]
Vamos entender como o código acima funciona:
- A função recebe dois argumentos: O primeiro (big_list) é a lista que você quer dividir e o segundo (sublist_size) é o tamanho das sublistas que você quer criar;
- Logo no início, a função pega o tamanho da lista passada (qty_el);
- É iniciado um for, utilizando a função range com três parâmetros que correspondem, respectivamente a: inicio, final e step (o quanto a lista é incrementada a cada iteração).
- Isso quer dizer que este for fará 4 iterações e o valor de i será (na sequência): 0, 10, 20 e 30;
- A cada loop, ele utiliza a keyword yield, passando um pedaço da big_list com a “fórmula”: big_list[i:i+sublist_size]
- A big_list ficará dividia da seguinte forma: big_list[0:10], big_list[10:20], big_list[20:30], big_list[30:40];
Nesta hora você pode estar se perguntando: Mas a lista não tem 31 elementos? Isso vai gerar um IndexError, não? A lista realmente tem apenas 31 elementos, mas quando utilizamos a sintaxe [30:40], não estamos acessando 1 posição específica do vetor, estamos pedindo uma faixa (slice) da lista e isso é interpretado como “me retorne todos os elementos que existem entre as posições 30 e 40”. Seria algo similar a fazer um “select * from tabela where id between 30 and 40;”, se não existirem ids maiores que 30, o banco de dados não vai retornar um erro. Ele vai retornar apenas os valores que estão disponíveis. Neste caso, a lógica é a mesma;
- A big_list ficará dividia da seguinte forma: big_list[0:10], big_list[10:20], big_list[20:30], big_list[30:40];
Nesta função, não utilizamos a keyword return, mas yield e isso vai nos retornar um objeto do tipo generator. Este tipo de objeto de utilização única, não possui os valores em si, mas uma série de instruções que serão executadas em um segundo momento.
Cada vez que o yield for chamado, ele vai armazenar um comando (no caso: big_list[0:10], big_list[10:20], big_list[20:30] e big_list[30:40]), mas não irá executá-los. Quando você converter este objeto para list, ele vai executar os comandos, mas ele só pode ser utilizado uma vez.
Colocando em prática:
gen = create_sublists(big_list=avengers, sublist_size=10)
No comando acima, criamos uma variável (gen) do tipo generator, que possui os comandos para criar as sublistas.
Para o que estamos nos proponde, basta gerar uma lista com o objeto do tipo generator como elemento:
print(list(gen))
O comando acima vai gerar o seguinte resultado:
[['Robert Downey Jr.', 'Chris Hemsworth', 'Mark Ruffalo', 'Chris Evans', 'Scarlett Johansson', 'Don Cheadle', 'Benedict Cumberbatch', 'Tom Holland', 'Chadwick Boseman', 'Zoe Saldana'], ['Karen Gillan', 'Tom Hiddleston', 'Paul Bettany', 'Elizabeth Olsen', 'Anthony Mackie', 'Sebastian Stan', 'Idris Elba', 'Danai Gurira', 'Peter Dinklage', 'Benedict Wong'],
['Pom Klementieff', 'Dave Bautista', 'Vin Diesel', 'Bradley Cooper', 'Gwyneth Paltrow', 'Benicio Del Toro', 'Josh Brolin', 'Chris Pratt', 'Sean Gunn', 'William Hurt'],
['Letitia Wright']]
Como vocês podem ver, foram geradas 3 listas: Duas com 10 elementos cada e uma com o elemento que “sobrou” dos 31 que existiam.
O que acontece se tentarmos executar o mesmo comando (print(list(gen))) novamente?
[]
O resultado vai ser uma lista vazia. Por que isso acontece?
Isso ocorre por que a variável gen era do tipo generator e ele guarda as instruções até que elas sejam executadas a primeira vez. Depois ele “esquece” estes comandos.
Qual a vantagem de utilizar o Yield? Bom, ele é uma espécie de “lazy loading” dos dados e (até onde consegui perceber) requer menos memória para operar. Nem sempre o yield vai ser a resposta para problemas de performance, mas vale a tentativa.
Abordagem alternativa
Se você não gostou da abordagem utilizando o yield, você pode fazer uma versão alternativa da função acima, que já vai gerar uma lista com as sublistas e retorna-la para você. Veja o exemplo:
def create_sublists_alt(big_list, sublist_size):
qty_el = len(big_list)
list_of_lists = []
for i in range(0, qty_el, sublist_size):
# Create an index range for l of n items:
list_of_lists.append(big_list[i:i+sublist_size])
return list_of_lists
O código acima funciona da mesma forma que explicamos anteriormente, mas ao invés de utilizar o yield, utilizamos a função append da class list.
Espero ter ajudado.
Referência: