Compreensões e geradores
Transformações e filtros em compreensões, percursos aninhados e consumo de iteradores.
Para obter os quadrados dos números positivos, percorres uma coleção, selecionas valores e transformas cada selecionado. Um ciclo escreve essas decisões em vários passos, enquanto uma compreensão junta-as numa expressão que constrói outra lista.
Compreensões de listas
A forma [expressao for x in xs if condicao] percorre xs, verifica a condição e acrescenta o valor da expressão apenas nos casos aceites.
valores = [-2, 0, 3, 4]
quadrados = [x * x for x in valores if x > 0]
print(quadrados)
print(valores)
print(["par" if x % 2 == 0 else "ímpar" for x in range(4)])Executa o código para ver a saída e os gráficos.
O filtro retira -2 e 0 e a transformação calcula 9 e 16, por isso o resultado tem duas posições. Na expressão a if condicao else b, o resultado é a quando a condição é verdadeira e b caso contrário; este if ... else escolhe o valor de cada elemento sem filtrar entradas, por isso o último exemplo conserva quatro posições.
A compreensão equivale a este ciclo:
quadrados = []
for x in valores:
if x > 0:
quadrados.append(x * x)
A compreensão constrói uma lista nova, mas isso não garante ausência de efeitos. Com xs = [[1], [2]], [linha.append(9) for linha in xs] altera xs para [[1, 9], [2, 9]] e produz [None, None]: cada chamada a append devolve None.
Usa a expressão para calcular valores novos. Para vários estados, validações ou efeitos, um ciclo pode mostrar melhor os passos.
Percursos aninhados
As cláusulas for seguem a mesma ordem dos ciclos correspondentes. Em [(i, j) for i in range(2) for j in range(3)], o i fica fixo enquanto j percorre 0, 1 e 2. O resultado tem seis pares: (0, 0), (0, 1), (0, 2), (1, 0), (1, 1) e (1, 2).
matriz = [[1, 2], [3, 4]]
plana = [valor for linha in matriz for valor in linha]
print(plana)
A saída é [1, 2, 3, 4]. Este código remove um nível de listas, não qualquer profundidade. Para estruturas com profundidade variável, usa a decomposição recursiva da lição de recursão.
Também podes criar conjuntos com {x * x for x in valores} e dicionários com {nome: len(nome) for nome in nomes}. Um conjunto elimina resultados repetidos. Num dicionário, uma chave repetida fica com o último valor produzido.
Pares de posições e fronteiras
Para comparar reservas duas a duas, percorremos i e só depois j > i.
Assim não comparamos uma reserva consigo própria nem repetimos o par ao
contrário. Para três posições, os pares são (0, 1), (0, 2) e (1, 2).
Uma reserva (inicio, fim) pode ocupar o intervalo semiaberto
: inclui o início e exclui o fim. Exigimos inicio < fim.
Duas reservas têm uma parte comum quando o maior início é estritamente
menor que o menor fim:
max(inicio_a, inicio_b) < min(fim_a, fim_b)
As reservas (9, 11) e (11, 12) apenas se tocam e são compatíveis.
Com (10, 12), a primeira partilha o intervalo e há conflito.
Se ambos os extremos fossem incluídos, o contacto num extremo já contaria
como interseção. A desigualdade depende desta convenção, não só dos números.
Iteráveis e iteradores
Um iterável permite obter um percurso; listas e strings são exemplos. Um iterador conserva a posição de um percurso e produz o próximo valor com next. iter(xs) cria um iterador e next(it) avança-o. Quando esgota, lança StopIteration, que for trata para terminar.
Uma lista permite começar um novo percurso. Um iterador esgotado não volta ao início:
it = iter([1, 2])
print(next(it))
print(list(it))
print(list(it))
A saída é 1, depois [2] e depois []; esta diferença de comportamento reaparece em map, filter e nos geradores.
Geradores e yield
Uma expressão (x * x for x in range(4)) cria um gerador, que produz valores quando são pedidos em vez de guardar logo uma lista.
quadrados = (x * x for x in range(4))
print(next(quadrados))
print(list(quadrados))
print(list(quadrados))
def contagem(n):
while n > 0:
yield n
n -= 1
print(list(contagem(3)))Executa o código para ver a saída e os gráficos.
next retira o zero e a primeira conversão em lista consome 1, 4 e 9, pelo que a segunda recebe uma sequência já esgotada.
Na função contagem, yield entrega um valor e suspende a execução; o próximo pedido retoma depois dessa instrução, conservando os nomes locais. Chamar contagem(3) cria o gerador, mas o corpo só começa no primeiro pedido, e return termina o gerador sem produzir mais valores.
Para seguir essa suspensão, considera g = contagem(3). Ao criar o gerador, o corpo ainda não correu. A tabela acompanha os pedidos seguintes; a última coluna mostra o valor local de n quando suspende ou termina:
| Pedido | Devolve | n |
|---|---|---|
1.º next | 3 | 3 |
2.º next | 2 | 2 |
3.º next | 1 | 1 |
4.º next | Exceção | 0 |
No primeiro pedido, o teste 3 > 0 permite chegar ao yield; nos três pedidos que entregam um valor, o estado fica suspenso depois do yield com o n indicado. O pedido seguinte faz n -= 1 antes de voltar ao teste, passando por 2 > 0 e 1 > 0 até 0 > 0, que é falso e faz o quarto next(g) lançar StopIteration.
A atualização ocorre depois de retomar o yield. Suspender conserva o ponto de execução; não é voltar a chamar a função desde o início.
Gerar valores de vários intervalos
Uma lista de intervalos inteiros inclusivos, como [(2, 3), (7, 7)], pode
representar o percurso 2, 3, 7 sem guardar todos os valores. Um gerador percorre
cada par e suspende a cada inteiro:
def expandir_intervalos(intervalos):
for inicio, fim in intervalos:
for valor in range(inicio, fim + 1):
yield valor
O contrato exige inicio <= fim em cada par. fim + 1 conserva o extremo
final e um intervalo de um só valor produz esse valor uma vez. A lista exterior
pode estar vazia, caso em que o gerador termina sem produzir valores. Segue a
ordem recebida; se os intervalos se sobrepuserem, os valores repetem-se.
O primeiro next de expandir_intervalos([(2, 3), (7, 7)]) produz 2;
converter o resto em lista dá [3, 7].
Os intervalos deste exemplo incluem os dois extremos. Não confundas essa representação com as reservas semiabertas da secção anterior.
Escolher entre lista e gerador
Usa uma lista quando precisas de índices, de vários percursos ou de guardar os resultados. Um gerador permite consumir um valor de cada vez, o que pode reduzir memória. Não reduz necessariamente o trabalho total se acabares por consumir tudo.
sum(x * x for x in range(100)) consome os valores sem precisar de uma lista intermédia. Se guardares o gerador e tentares somá-lo duas vezes, a segunda soma vale zero. Criar um novo gerador inicia um novo percurso. O custo da expressão que calcula cada valor continua a existir, mesmo quando a produção é adiada.
Exercícios
Separar filtro e transformação
Prevê [x + 1 for x in [-1, 0, 2] if x >= 0] e [
x + 1 if x >= 0 else 0 for x in [-1, 0, 2]].
Pista
Mais uma pista
Solução
A primeira produz [1, 3]. A segunda produz
[0, 1, 3], conservando uma posição para -1.
Confere a tua resposta:
- Obtive [1, 3].
- Obtive [0, 1, 3].
- Expliquei a diferença de comprimentos.
Erros frequentes
Prever consumo parcial
g = (x * 2 for x in range(3)). Depois de
next(g), prevê list(g) e uma segunda
list(g).
Pista
next consome o primeiro valor, zero.
Mais uma pista
O segundo percurso usa o mesmo iterador, já esgotado pelo primeiro
list.
Solução
O primeiro list(g) dá [2, 4]. O segundo dá
[]. O zero foi consumido por next; criar
outro gerador seria necessário para repetir desde o início.
Confere a tua resposta:
- Retirei o zero consumido por next.
- Obtive [2, 4] no primeiro list.
- Obtive [] no segundo.
Erros frequentes
Pensar que list reinicia um iterador.
Escolher entre guardar e recalcular
Uma transformação demorada produz pelo menos 11 resultados. Queres percorrê-los três vezes e consultar o resultado no índice 10, sem recalcular a transformação. Escolhes uma lista ou um gerador? Explica por que criar outro gerador não satisfaz a última condição. Se só quisesses somar os resultados uma vez, que escolha permitiria evitar guardá-los todos?
Solução
Uma lista guarda os resultados, permite novos percursos e acesso por índice. Um gerador único não permite índices nem reinicia quando esgota. Criar outro gerador repete a transformação, contrariando o requisito de não recalcular.
Para uma soma única, sum pode consumir um gerador valor a valor, sem uma lista
de todos os resultados. Isso evita essa memória intermédia, mas não elimina o
trabalho da transformação. A entrada e o estado do gerador continuam a ocupar
memória.
Confere a tua resposta:
- Escolhi uma lista para índice e percursos repetidos.
- Expliquei que um gerador novo repete o cálculo.
- Escolhi consumo progressivo para uma soma única.
Filtrar numa lista de listas
Define quadrados_positivos(linhas): recebe uma lista de listas de números e
devolve uma lista plana com os quadrados dos valores estritamente positivos.
Percorre as linhas e os valores pela ordem original. As linhas podem ter
comprimentos diferentes; conserva a entrada. Experimenta uma compreensão.
def quadrados_positivos(linhas):
return []Verifica o código para comparar o resultado com os testes.
Testes
Valores negativos, zero e linhas diferentes
Código do testeprint(quadrados_positivos([[-2, 3], [], [0, 4, 1]]))Esperado:[9, 16, 1]
Sem linhas
Código do testeprint(quadrados_positivos([]))Esperado:[]
Nenhum positivo
Código do testeprint(quadrados_positivos([[-1, 0], []]))Esperado:[]
Repetições e decimais
Código do testeprint(quadrados_positivos([[0.5, 2], [2]]))Esperado:[0.25, 4, 4]
Entrada preservada
Código do testexs = [[-2, 3], [1]] print(quadrados_positivos(xs)) print(xs)Esperado:[9, 1] [[-2, 3], [1]]
print(quadrados_positivos([[-2, 3], [], [0, 4, 1]]))[9, 16, 1]
print(quadrados_positivos([]))[]
print(quadrados_positivos([[-1, 0], []]))[]
print(quadrados_positivos([[0.5, 2], [2]]))[0.25, 4, 4]
xs = [[-2, 3], [1]]
print(quadrados_positivos(xs))
print(xs)[9, 1] [[-2, 3], [1]]
Pista
Há duas decisões de percurso: escolher a linha e depois o valor dessa linha.
Mais uma pista
Escreve primeiro for linha in linhas, depois for x in linha. O filtro x > 0 decide sobre o valor original.
Solução
O primeiro for percorre as linhas; o segundo percorre cada linha. O filtro exclui negativos e zero antes de calcular o quadrado. Linhas vazias não acrescentam valores.
def quadrados_positivos(linhas):
return [x * x for linha in linhas for x in linha if x > 0]Erros frequentes
Filtrar o quadrado em vez do valor original, incluir zero ou assumir que todas as linhas têm o mesmo comprimento.
Diagnosticar uma compreensão com efeitos
Prevê xs e resultado depois deste trecho. Substitui a compreensão por uma que devolva [[1, 9], [2, 9]] e mantenha xs como [[1], [2]]. Justifica por que construir uma lista exterior nova não chega para preservar a entrada.
xs = [[1], [2]]
resultado = [linha.append(9) for linha in xs]Pista
append.Solução
xs passa a [[1, 9], [2, 9]], porque linha refere cada lista interior. Cada append devolve None, logo resultado é [None, None]. A lista exterior nova guarda os retornos dessas chamadas; não desfaz os efeitos nas linhas.
Partindo novamente de xs = [[1], [2]], a correção é resultado = [linha + [9] for linha in xs]. A concatenação cria uma lista nova por linha e não altera as listas recebidas. Com xs = [], o resultado é [].
Confere a tua resposta:
- Previ xs como [[1, 9], [2, 9]].
- Previ resultado como [None, None].
- Construí listas novas e preservei a entrada na correção.
Comparar reservas sem duplicar pares
Define conflitos(reservas): cada reserva é um par de inteiros (inicio, fim),
com inicio < fim, que ocupa o intervalo semiaberto . Devolve
uma lista dos pares de índices (i, j) de reservas que partilham tempo, com
i < j, por ordem lexicográfica. Uma saída às 11 permite outra entrada às 11.
Não ordenes nem alteres a lista recebida. Constrói uma compreensão e justifica
a desigualdade usada para decidir o conflito.
def conflitos(reservas):
return []Verifica o código para comparar o resultado com os testes.
Testes
Contactos e sobreposições
Código do testeprint(conflitos([(9, 11), (11, 12), (10, 12), (8, 9)]))Esperado:[(0, 2), (1, 2)]
Inclusão e intervalos iguais
Código do testeprint(conflitos([(0, 5), (1, 2), (0, 5)]))Esperado:[(0, 1), (0, 2), (1, 2)]
Nenhum par possível
Código do testeprint(conflitos([]), conflitos([(0, 1)]))Esperado:[] []
Entrada intacta e ordem dos índices
Código do testexs = [(5, 7), (0, 2), (1, 6)] print(conflitos(xs)) print(xs)Esperado:[(0, 2), (1, 2)] [(5, 7), (0, 2), (1, 6)]
print(conflitos([(9, 11), (11, 12), (10, 12), (8, 9)]))[(0, 2), (1, 2)]
print(conflitos([(0, 5), (1, 2), (0, 5)]))[(0, 1), (0, 2), (1, 2)]
print(conflitos([]), conflitos([(0, 1)]))[] []
xs = [(5, 7), (0, 2), (1, 6)]
print(conflitos(xs))
print(xs)[(0, 2), (1, 2)] [(5, 7), (0, 2), (1, 6)]
Pista
Solução
O maior início tem de ficar antes do menor fim. Na igualdade, os intervalos
apenas se tocam e o extremo final está excluído. Os percursos com j > i
visitam cada par uma vez, já na ordem pedida.
def conflitos(reservas):
return [
(i, j)
for i in range(len(reservas))
for j in range(i + 1, len(reservas))
if max(reservas[i][0], reservas[j][0])
< min(reservas[i][1], reservas[j][1])
]Na primeira entrada, 0 e 1 tocam-se às 11 sem conflito. A reserva 2 partilha
com 0 e com 1. A reserva 3 só toca 0 às 9.
Na segunda, a inclusão total também conta como conflito. Ordenar as reservas
alteraria o significado dos índices; usar <= contaria contactos indevidos.
Produzir um percurso a pedido
Define o gerador expandir_intervalos(intervalos): recebe uma lista de pares
inteiros (inicio, fim) com inicio <= fim. Produz cada inteiro desses
intervalos, incluindo ambos os extremos, pela ordem dos pares e em ordem
crescente dentro de cada par. Conserva repetições entre intervalos e não altera
a entrada. Usa yield, sem construir uma lista de todos os inteiros.
Os testes verificam o percurso e o consumo; o uso de memória também exige
conferir a solução.
def expandir_intervalos(intervalos):
yield 0Verifica o código para comparar o resultado com os testes.
Testes
Extremos, repetição e negativos
Código do testeprint(list(expandir_intervalos([(-1, 1), (1, 1), (4, 5)])))Esperado:[-1, 0, 1, 1, 4, 5]
Percurso vazio
Código do testeprint(list(expandir_intervalos([])))Esperado:[]
Consumo parcial e esgotamento
Código do testeg = expandir_intervalos([(2, 3), (7, 7)]) print(next(g)) print(list(g)) print(list(g))Esperado:2 [3, 7] []
Percurso sem reordenar nem alterar a entrada
Código do testexs = [(3, 3), (0, 1)] print(list(expandir_intervalos(xs))) print(xs)Esperado:[3, 0, 1] [(3, 3), (0, 1)]
print(list(expandir_intervalos([(-1, 1), (1, 1), (4, 5)])))[-1, 0, 1, 1, 4, 5]
print(list(expandir_intervalos([])))[]
g = expandir_intervalos([(2, 3), (7, 7)])
print(next(g))
print(list(g))
print(list(g))2 [3, 7] []
xs = [(3, 3), (0, 1)]
print(list(expandir_intervalos(xs)))
print(xs)[3, 0, 1] [(3, 3), (0, 1)]
Solução
def expandir_intervalos(intervalos):
for inicio, fim in intervalos:
for valor in range(inicio, fim + 1):
yield valorrange não guarda todos os inteiros, e o yield suspende o percurso interior
em cada valor. Ao retomar, continua nesse intervalo; ao terminá-lo, avança
para o par seguinte. O limite fim + 1 inclui o extremo final.
Depois de consumir 2 no terceiro teste, ficam 3 e 7; a conversão em lista
esgota-os, pelo que a segunda dá []. Devolver uma lista em vez de um
iterador faria next(g) falhar. Um intervalo de um valor e a lista exterior
vazia não exigem casos especiais neste percurso.