Compreensões e geradores

Transformações e filtros em compreensões, percursos aninhados e consumo de iteradores.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

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.

Compreender o filtro e a transformação
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)])
Resultado

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 [inicio,fim[[inicio,fim[: 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 [10,11[[10,11[ 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.

Consumir um gerador
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)))
Resultado

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:

PedidoDevolven
1.º next33
2.º next22
3.º next11
4.º nextExceção0

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
Na primeira expressão, o if retira posições.
Mais uma pista
Na segunda, o if escolhe o valor em cada posição.
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

Tratar o if da expressão como um filtro exterior.
Voltar à explicação
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.

Voltar à explicação
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.
Voltar à explicação
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.

Python
def quadrados_positivos(linhas):
    return []
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Valores negativos, zero e linhas diferentes
    Código do teste
    print(quadrados_positivos([[-2, 3], [], [0, 4, 1]]))
    Esperado:
    [9, 16, 1]
  2. Sem linhas
    Código do teste
    print(quadrados_positivos([]))
    Esperado:
    []
  3. Nenhum positivo
    Código do teste
    print(quadrados_positivos([[-1, 0], []]))
    Esperado:
    []
  4. Repetições e decimais
    Código do teste
    print(quadrados_positivos([[0.5, 2], [2]]))
    Esperado:
    [0.25, 4, 4]
  5. Entrada preservada
    Código do teste
    xs = [[-2, 3], [1]]
    print(quadrados_positivos(xs))
    print(xs)
    Esperado:
    [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.

Voltar à explicação
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
Segue separadamente o efeito e o retorno de cada 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.
Voltar à explicaçã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 [inicio,fim[[inicio,fim[. 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.

Python
def conflitos(reservas):
    return []
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Contactos e sobreposições
    Código do teste
    print(conflitos([(9, 11), (11, 12), (10, 12), (8, 9)]))
    Esperado:
    [(0, 2), (1, 2)]
  2. Inclusão e intervalos iguais
    Código do teste
    print(conflitos([(0, 5), (1, 2), (0, 5)]))
    Esperado:
    [(0, 1), (0, 2), (1, 2)]
  3. Nenhum par possível
    Código do teste
    print(conflitos([]), conflitos([(0, 1)]))
    Esperado:
    [] []
  4. Entrada intacta e ordem dos índices
    Código do teste
    xs = [(5, 7), (0, 2), (1, 6)]
    print(conflitos(xs))
    print(xs)
    Esperado:
    [(0, 2), (1, 2)]
    [(5, 7), (0, 2), (1, 6)]

Pista
A zona comum começa no maior início e acaba no menor fim.
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 [10,11[[10,11[ com 0 e [11,12[[11,12[ 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.

Voltar à explicação
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.

Python
def expandir_intervalos(intervalos):
    yield 0
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Extremos, repetição e negativos
    Código do teste
    print(list(expandir_intervalos([(-1, 1), (1, 1), (4, 5)])))
    Esperado:
    [-1, 0, 1, 1, 4, 5]
  2. Percurso vazio
    Código do teste
    print(list(expandir_intervalos([])))
    Esperado:
    []
  3. Consumo parcial e esgotamento
    Código do teste
    g = expandir_intervalos([(2, 3), (7, 7)])
    print(next(g))
    print(list(g))
    print(list(g))
    Esperado:
    2
    [3, 7]
    []
  4. Percurso sem reordenar nem alterar a entrada
    Código do teste
    xs = [(3, 3), (0, 1)]
    print(list(expandir_intervalos(xs)))
    print(xs)
    Esperado:
    [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 valor

range 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.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.