Listas: criação, manipulação e métodos

Listas: criação, manipulação e métodos

A estrutura que aparece em praticamente todo programa Python: criação, índices negativos, fatiamento, os métodos que alteram no lugar e as funções que não alteram. Com as duas armadilhas que custam caro — copiar uma lista aninhada sem perceber que a cópia é rasa, e montar uma matriz multiplicando listas.
Python

• • 12 min de leitura

Listas são a estrutura de dados mais utilizada em Python. Elas permitem armazenar múltiplos valores em uma única variável, de forma ordenada e mutável. Dominar listas é essencial — praticamente todo programa Python as utiliza de alguma forma.

Criando Listas

# Lista de strings
linguagens = ["Python", "JavaScript", "Go", "Rust"]

# Lista de inteiros
notas = [7, 8, 9, 10, 6]

# Lista mista
dados = ["Ricardo", 35, 1.78, True]

# Lista vazia
vazia = []

# Lista criada com range
sequencia = list(range(1, 6))
print(sequencia)  # [1, 2, 3, 4, 5]

Acessando Elementos

Listas usam índices baseados em zero. Python também suporta índices negativos — que contam a partir do final:

frutas = ["maçã", "banana", "laranja", "uva", "manga"]

print(frutas[0])   # maçã   — primeiro
print(frutas[2])   # laranja
print(frutas[-1])  # manga  — último
print(frutas[-2])  # uva    — penúltimo

Fatiamento (Slicing)

Extrair sublistas com a notação [start:stop:step]:

numeros = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(numeros[2:6])    # [2, 3, 4, 5]
print(numeros[:4])     # [0, 1, 2, 3]
print(numeros[6:])     # [6, 7, 8, 9]
print(numeros[::2])    # [0, 2, 4, 6, 8] — de 2 em 2
print(numeros[::-1])   # [9, 8, 7, ..., 0] — invertida

O fatiamento nunca gera erro de índice — se o limite ultrapassar o tamanho da lista, Python simplesmente vai até o fim.

Modificando Listas

Listas são mutáveis — seus elementos podem ser alterados após a criação:

cores = ["vermelho", "verde", "azul"]

# Alterando um elemento
cores[1] = "amarelo"
print(cores)  # ['vermelho', 'amarelo', 'azul']

# Alterando um intervalo
cores[0:2] = ["rosa", "roxo"]
print(cores)  # ['rosa', 'roxo', 'azul']

Métodos Principais

lista = [3, 1, 4, 1, 5, 9, 2, 6]

# Adicionar ao final
lista.append(7)

# Inserir em posição específica
lista.insert(0, 0)       # insere 0 no índice 0

# Remover por valor
lista.remove(1)          # remove a primeira ocorrência de 1

# Remover por índice e retornar o valor
ultimo = lista.pop()     # remove e retorna o último
terceiro = lista.pop(2)  # remove e retorna o índice 2

# Ordenar in-place
lista.sort()
lista.sort(reverse=True)  # ordem decrescente
# ATENCAO: sort() ordena no lugar e devolve None.
# Escrever  lista = lista.sort()  deixa lista valendo None.

# Inverter in-place
lista.reverse()

# Contar ocorrências
print(lista.count(1))

# Localizar índice
print(lista.index(5))

# Limpar a lista
lista.clear()

Funções Nativas com Listas

notas = [7.5, 8.0, 9.5, 6.0, 10.0]

print(len(notas))    # 5    — quantidade de elementos
print(sum(notas))    # 41.0 — soma
print(min(notas))    # 6.0  — menor valor
print(max(notas))    # 10.0 — maior valor

# Ordenar sem modificar a original
ordenadas = sorted(notas)
ordenadas_desc = sorted(notas, reverse=True)

Copiando Listas

Um erro comum é tentar copiar uma lista com = — e o motivo já apareceu em Variáveis e Tipos de Dados: o nome é um rótulo colado num objeto, e atribuir um nome a outro só cola um segundo rótulo no mesmo objeto.

original = [1, 2, 3]
copia = original       # não é uma cópia — é o mesmo objeto!

copia.append(4)
print(original)        # [1, 2, 3, 4] — original também mudou!

Para copiar corretamente:

# Forma 1 — método copy()
copia = original.copy()

# Forma 2 — fatiamento
copia = original[:]

# Forma 3 — construtor list()
copia = list(original)

As três formas têm o mesmo limite, e ele importa muito daqui a pouco, quando aparecerem as listas aninhadas: todas são cópias rasas. Elas criam uma lista nova, mas os elementos continuam sendo os mesmos objetos. Enquanto a lista guardar números ou textos isso não faz diferença, porque esses tipos são imutáveis e ninguém altera um deles no lugar. Quando a lista guarda outras listas, a história muda:

matriz = [[1, 2], [3, 4]]
copia = matriz.copy()      # ou matriz[:], ou list(matriz)

copia[0][0] = 99
print(matriz)              # [[99, 2], [3, 4]] — a original mudou!

A lista de fora foi copiada; as de dentro, não. Para uma cópia de verdade em qualquer profundidade existe o deepcopy:

import copy

copia = copy.deepcopy(matriz)
copia[0][0] = 99
print(matriz)              # [[1, 2], [3, 4]] — intacta

Não saia usando deepcopy por precaução, no entanto: ele percorre a estrutura inteira e é bem mais caro. A cópia rasa basta na maioria dos casos — o que não basta é ignorar a diferença.

List Comprehension

Uma das características mais elegantes do Python — cria listas de forma compacta e expressiva:

# Forma tradicional
quadrados = []
for i in range(1, 6):
    quadrados.append(i ** 2)

# Com list comprehension
quadrados = [i ** 2 for i in range(1, 6)]
print(quadrados)  # [1, 4, 9, 16, 25]

# Com filtro
pares = [i for i in range(20) if i % 2 == 0]
print(pares)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Transformando strings
nomes = ["ana", "bruno", "carla"]
maiusculos = [nome.upper() for nome in nomes]
print(maiusculos)  # ['ANA', 'BRUNO', 'CARLA']

Listas Aninhadas (Matrizes)

Listas podem conter outras listas — útil para representar matrizes ou tabelas:

matriz = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

print(matriz[1][2])  # 6 — linha 1, coluna 2

# CUIDADO: multiplicar uma lista NAO cria linhas independentes
ruim = [[0] * 3] * 3
ruim[0][0] = 5
print(ruim)          # [[5, 0, 0], [5, 0, 0], [5, 0, 0]] — as tres!

# a forma correta cria uma lista nova a cada volta
bom = [[0] * 3 for _ in range(3)]
bom[0][0] = 5
print(bom)           # [[5, 0, 0], [0, 0, 0], [0, 0, 0]]

# Percorrendo uma matriz
for linha in matriz:
    for elemento in linha:
        print(elemento, end=" ")
    print()
# 1 2 3
# 4 5 6
# 7 8 9

Exemplo Completo: Sistema de Notas

def calcular_estatisticas(notas):
    """Calcula média, maior e menor nota de uma lista."""
    if not notas:
        return None

    media = sum(notas) / len(notas)
    aprovados = [n for n in notas if n >= 6.0]
    reprovados = [n for n in notas if n < 6.0]

    return {
        "total": len(notas),
        "media": round(media, 2),
        "maior": max(notas),
        "menor": min(notas),
        "aprovados": len(aprovados),
        "reprovados": len(reprovados),
    }

notas_turma = [8.5, 4.0, 7.0, 9.5, 5.5, 6.0, 3.0, 10.0]
stats = calcular_estatisticas(notas_turma)

for chave, valor in stats.items():
    print(f"{chave:>12}: {valor}")

Duas confusões explicam a maior parte dos erros que se comete com listas, e as duas vêm da mesma origem: um nome é um rótulo, não uma caixa. A primeira é achar que atribuir copia — não copia, cola um segundo rótulo no mesmo objeto. A segunda é achar que, resolvida a primeira, o problema acabou — mas copy(), [:] e list() copiam só o primeiro nível, e uma lista de listas continua compartilhando o que tem dentro. Enquanto os elementos forem números e textos isso nunca aparece, porque eles são imutáveis; no dia em que forem listas ou dicionários, aparece de uma vez.

A outra linha divisória vale a pena guardar porque organiza dezenas de métodos: o que altera no lugar devolve None, e o que devolve algo não altera. lista.sort(), lista.reverse() e lista.append() estão de um lado; sorted(), reversed() e o fatiamento estão do outro. Não é arbitrário — é uma convenção do Python para que nunca se escreva por engano uma cadeia de chamadas que modifica a lista original no caminho. O preço é o erro de principiante lista = lista.sort(), que deixa a variável valendo None e some com os dados.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Uma função recebe um gabarito de prova como lista de listas e devolve uma versão corrigida, fazendo novo = gabarito.copy() antes de alterar. O gabarito original aparece alterado mesmo assim. O que houve?

Ver resposta

✓ Resposta: A cópia é rasa. O copy() cria uma lista nova, mas preenche essa lista com os mesmos objetos que estavam na original — e, quando os elementos são listas, as duas coleções passam a apontar para as mesmas sublistas. Alterar novo[0][2] alcança exatamente a mesma lista interna que gabarito[0][2]. O que a cópia protege é só o primeiro nível: novo.append(...) ou novo[0] = outra_coisa não afetam o original, porque aí se está mexendo na lista de fora. As três formas que o artigo apresenta — copy(), [:] e list() — têm todas esse mesmo comportamento; não existe diferença entre elas nesse ponto. Enquanto a lista guardar números, textos ou tuplas de imutáveis, o problema nunca aparece, e é justamente por isso que ele surpreende: o código funcionou durante meses com listas simples. A solução, quando se precisa mesmo de independência em qualquer profundidade, é copy.deepcopy(gabarito), que reconstrói a estrutura inteira. Ele não deve ser o padrão, porém, porque percorre tudo e custa caro em estrutura grande. Existe uma terceira via, quase sempre melhor que as duas: não copiar — construir a estrutura nova com uma compreensão, novo = [linha.copy() for linha in gabarito] quando há um nível, ou devolver dados novos em vez de alterar os recebidos.

Exercício 2

Para criar um tabuleiro 3 por 3 zerado, um colega escreve tabuleiro = [[0] * 3] * 3. Ao marcar uma jogada em tabuleiro[0][0], três casas mudam. Explique, e mostre a forma correta.

Ver resposta

✓ Resposta: O operador * aplicado a uma lista repete referências, não conteúdo. A expressão de dentro, [0] * 3, funciona como se espera e produz [0, 0, 0], porque os elementos são inteiros imutáveis. A de fora é que engana: ela pega aquela única lista e a repete três vezes — o resultado tem três posições que apontam para o mesmo objeto. Escrever em tabuleiro[0][0] altera a lista compartilhada, e as três "linhas" mostram a mudança, porque são a mesma linha vista três vezes. Dá para confirmar comparando as identidades: tabuleiro[0] is tabuleiro[1] devolve True. A forma correta é a compreensão de lista, que executa a expressão a cada volta e portanto cria uma lista nova por linha: tabuleiro = [[0] * 3 for _ in range(3)]. O sublinhado é a convenção para a variável de laço que não se usa. Vale notar que o problema não é do * em si — [0] * 1000 é a forma idiomática e eficiente de criar uma lista grande de zeros. O problema é multiplicar uma lista cujo elemento é mutável. A mesma armadilha aparece ao usar dict.fromkeys(chaves, []), onde todas as chaves recebem a mesmíssima lista.

Exercício 3

Um processamento consome itens de uma fila usando fila.pop(0) num laço. Com centenas de itens é instantâneo; com centenas de milhares, trava. A lógica está correta. O que explica a lentidão e qual é a alternativa?

Ver resposta

✓ Resposta: A lista do Python é um vetor contíguo: os elementos ficam lado a lado na memória, e é isso que torna o acesso por índice instantâneo. O preço é nas pontas. Remover do fim com pop() é imediato, porque nada precisa se mover; remover do começo com pop(0) obriga a deslocar todos os outros uma posição para trás, e o custo é proporcional ao tamanho da lista. Num laço que esvazia a fila inteira, isso se acumula: o trabalho total cresce com o quadrado do número de itens. Por isso dobrar a entrada não dobra o tempo — quadruplica. É o padrão clássico de código que passa em todos os testes com dados pequenos e morre em produção. A alternativa é collections.deque, uma fila de duas pontas feita exatamente para isso, com popleft() e appendleft() de custo constante — na medição, esvaziar vinte mil itens levou vinte e sete vezes menos tempo que com a lista. A mesma observação vale para insert(0, x), que tem o mesmo defeito na inserção. A lição mais ampla é escolher a estrutura pelo padrão de acesso, não pelo hábito: lista para acessar por índice e crescer no fim, deque para mexer nas duas pontas, set para perguntar se um item existe, dict para associar chave a valor.

Exercício 4

O artigo afirma que o fatiamento nunca gera erro de índice. Isso é conveniente ou perigoso? Compare lista[5] com lista[5:6] numa lista de três elementos.

Ver resposta

✓ Resposta: É as duas coisas, e saber qual delas se quer é o ponto. Numa lista de três elementos, lista[5] levanta IndexError e interrompe o programa ali, apontando o problema; lista[5:6] devolve [], sem reclamar. A conveniência é real e usada o tempo todo: texto[:100] pega os cem primeiros caracteres sem precisar verificar se existem cem, e lista[1:] devolve [] numa lista de um elemento, em vez de exigir um if. O perigo é que o silêncio esconde erro de lógica. Um índice calculado errado — um deslocamento trocado, uma conta de paginação furada — produz uma fatia vazia que segue adiante como se fosse resultado legítimo, e o programa termina normalmente entregando nada. O sintoma aparece páginas depois, como "o relatório veio vazio", sem nenhuma pista da causa. A regra prática: use fatiamento quando a lista poder mesmo ser menor do que o pedido, e índice direto quando o elemento tiver que existir — deixando o IndexError fazer o trabalho de avisar. E vale conhecer duas pegadinhas do fatiamento que decorrem da mesma tolerância: lista[::-1] inverte, mas lista[3:0] devolve [] em vez de inverter o trecho, e passo negativo exige inverter também os limites.

Exercício 5

Reescreva resultado = [] seguido de um for com if e append como uma compreensão de lista. Depois explique quando não vale a pena fazer essa troca.

Ver resposta

✓ Resposta: A forma é resultado = [transformar(x) for x in origem if condicao(x)], lida na mesma ordem do laço: primeiro o for, depois o filtro, e a expressão da frente é o que entra na lista. Além de mais curta, é um pouco mais rápida, porque não paga a busca e a chamada do método append a cada volta, e é mais segura, porque a variável do laço não vaza para fora — coisa que acontece no for comum. Não vale a pena em quatro situações. Quando o corpo faz mais de uma coisa: se além de montar a lista ele registra log, atualiza um contador ou grava algo, o laço explícito diz a verdade sobre o que está acontecendo. Quando a expressão exige condicional embutida aninhada — [a if c1 else b if c2 else d for x in y] é legal e ilegível. Quando há dois ou mais for encadeados e a ordem deles deixa de ser óbvia à primeira leitura. E quando o propósito não é produzir uma lista: usar compreensão só pelo efeito colateral, descartando o resultado, é desperdiçar memória construindo uma lista que ninguém vai ler. Duas variações que valem conhecer: trocar os colchetes por parênteses cria um gerador, que não constrói nada na memória e serve perfeitamente para alimentar um sum() ou um any(); e a mesma sintaxe com chaves produz conjunto ou dicionário, conforme se escreva um valor ou um par.

Comentários

Mais em Python

Trabalhando com Datas e Horas
Trabalhando com Datas e Horas

Datas e horas em Python com datetime, timedelta, zoneinfo e dateutil. Com as…

Tuplas e Sets: imutabilidade e unicidade
Tuplas e Sets: imutabilidade e unicidade

Tuplas e sets parecem variações da lista, mas resolvem problemas distintos…

Tratamento de Exceções e Erros
Tratamento de Exceções e Erros

Exceções em Python, do try básico ao encadeamento com from. Incluindo três…