Tuplos e listas
Sequências, desempacotamento, operações, mutação, alias e cópia superficial.
As notas [12, 15] formam uma lista, uma sequência onde podemos acrescentar 18 ou substituir 12 por 20, enquanto o tuplo (12, 15) conserva os valores por ordem sem permitir alterar as suas posições; essa diferença chama-se mutabilidade.
Sequências e tuplos
Strings, listas e tuplos suportam len, índices, fatias e pertença com in. Uma lista escreve-se [12, 15]; um tuplo pode escrever-se (12, 15). A vírgula é que cria o tuplo: (12,) tem um elemento, mas (12) é apenas o inteiro 12 entre parênteses.
O desempacotamento associa elementos a nomes: x, y = (3, 4). Nesta forma, o número de nomes tem de coincidir com o número de elementos. a, b = b, a troca os valores porque o lado direito é calculado antes das atribuições.
Um tuplo é imutável, mas pode conter objetos mutáveis. Em registo = ("Ana", [12, 15]), não podes substituir registo[0], mas podes alterar a lista registo[1]. Imutabilidade do recipiente não significa imutabilidade de todos os objetos alcançáveis a partir dele.
Alterar uma lista
Segue uma lista concreta desde o início:
xs = [10, 20]
xs.append(30)
xs[0] = 11
print(xs)
A lista passa por [10, 20], depois [10, 20, 30] e depois [11, 20, 30]. append altera a própria lista; a atribuição por índice substitui um elemento existente.
| Operação | Efeito |
|---|---|
xs.append(x) | Acrescenta um elemento, mesmo que x seja uma lista |
xs.extend(ys) | Acrescenta cada elemento de ys |
xs.insert(i, x) | Insere antes da posição i |
xs.pop(i) | Retira e devolve o elemento dessa posição |
xs.remove(x) | Retira a primeira ocorrência de x; falha se faltar |
del xs[i] | Retira o elemento sem o devolver |
xs[a:b] = ys | Substitui uma fatia, podendo mudar o comprimento |
append, extend, insert, remove, sort e reverse devolvem None. Não escrevas xs = xs.append(4), porque acabarias com xs associado a None.
Por exemplo, ys = xs.append(4) guarda None em ys e altera xs. xs + ys cria outra lista. Não alteres o comprimento da lista que estás a percorrer sem uma estratégia explícita; retirar um elemento pode deslocar o seguinte e fazê-lo escapar ao ciclo.
Alias contra cópia, igualdade contra identidade
b = a não copia a lista. Cria outro nome para o mesmo objeto, um alias. a == b compara conteúdos; a is b testa se os nomes referem o mesmo objeto.
notas = [12, 15]
alias = notas
copia = notas.copy()
alias.append(18)
copia[0] = 20
print(notas)
print(copia)
print(alias is notas, copia is notas)
print([1, 2] == [1, 2])Executa o código para ver a saída e os gráficos.
append altera a lista partilhada por notas e alias, enquanto a alteração em copia atinge outra lista. Dois objetos distintos podem ter conteúdo igual.
A identidade ajuda a seguir a partilha, mas um cálculo que só depende dos valores não deve depender dela.
Listas de listas e cópia superficial
a.copy() e a[:] criam uma cópia superficial. Copiam o recipiente, mantendo referências aos elementos. Se os elementos forem listas, continuam partilhados.
matriz = [[1, 2], [3, 4]]
copia = matriz.copy()
copia[0][0] = 9
print(matriz)
print(copia)Executa o código para ver a saída e os gráficos.
As duas primeiras saídas contêm o 9 porque a primeira linha é partilhada. Distingue as duas operações sobre a cópia: copia[0] = [9, 9] substituiria a referência guardada na cópia e deixaria a matriz intacta, enquanto copia[0][0] = 9 altera a linha partilhada e aparece nas duas listas. Para copiar cada linha sem usar ainda compreensões, constrói a lista nova com um ciclo:
independente = []
for linha in matriz:
independente.append(linha.copy())
Cada iteração acrescenta a cópia de uma linha. Se executarmos depois independente[0][0] = 7, a matriz original mantém [[9, 2], [3, 4]] e a nova fica [[7, 2], [3, 4]]. Os elementos das linhas são inteiros, por isso a alteração já é independente. Para estruturas mais profundas, copiar um só nível adicional pode não chegar. A forma equivalente com compreensão será explicada em compreensões e geradores.
O esquema mostra o estado após copia[0][0] = 9: cada caixa é um objeto e cada seta é uma referência a esse objeto. As listas exteriores são distintas, mas as referências nas posições 0 e 1 de ambas chegam às mesmas listas interiores.
Por isso, alterar um elemento da primeira linha através de copia também muda o que vemos através de matriz.
Evita [[0] * 3] * 2 para criar duas linhas independentes. A repetição exterior repete a referência à mesma linha. Quando precisares de uma linha nova em cada posição, cria cada linha dentro de um ciclo; a versão com compreensão aparece na lição própria.
Ver a mesma construção com um ciclo
matriz = []
for i in range(2):
matriz.append([0] * 3)
matriz[0][0] = 5
print(matriz)O resultado é [[5, 0, 0], [0, 0, 0]], porque criar a linha dentro do ciclo impede a partilha.
Percorrer e combinar sequências
enumerate(xs) produz pares com índice e valor. zip(xs, ys) combina elementos na mesma posição e, por defeito, termina na sequência mais curta. Se o contrato exige comprimentos iguais, verifica-o antes de combinar.
O produto interno de dois vetores com o mesmo comprimento é a soma dos produtos correspondentes. Para [2, 3] e [4, 5], calculamos . Para dois vetores vazios, a soma é zero.
def produto_interno(u, v):
if len(u) != len(v):
raise ValueError("comprimentos diferentes")
soma = 0
for a, b in zip(u, v):
soma += a * b
return soma
print(produto_interno([2, 3], [4, 5]))
O resultado é 23; raise comunica uma entrada inválida e o tratamento dessa exceção está explicado na última lição do percurso.
Ordenar sem confundir os resultados
sorted(xs) devolve uma lista nova e deixa xs intacta. xs.sort() ordena a lista existente e devolve None. reverse=True pede ordem decrescente. Para ordenar registos por um campo, usa key; a lição de programação funcional explica como passar essa função.
Exercícios
Seguir o efeito e o retorno de um método
Prevê os conteúdos de xs e retorno depois destas instruções. Depois
explica o que muda se substituíres a primeira chamada por
xs = xs.append([2, 3]), incluindo o efeito na instrução seguinte.
xs = [1]
retorno = xs.append([2, 3])
xs.extend([4, 5])Solução
append acrescenta a lista [2, 3] como um único elemento, pelo que xs
passa a [1, [2, 3]]. O retorno do método é None, guardado em retorno.
extend acrescenta separadamente 4 e 5, produzindo [1, [2, 3], 4, 5].
Na substituição, a chamada ainda altera a lista, mas a atribuição associa
xs ao retorno None. A instrução xs.extend([4, 5]) lança então
AttributeError, porque None não tem esse método. Para conservar a lista,
chama o método sem atribuir o seu retorno a xs.
Confere a tua resposta:
- Obtive [1, [2, 3], 4, 5] e None.
- Distingui append de extend.
- Expliquei a perda do nome da lista na atribuição incorreta.
Combinar vetores com o mesmo comprimento
Define produto_interno(u, v): recebe duas listas de números e devolve a soma
dos produtos dos elementos na mesma posição, sem alterar as entradas. A soma
de dois vetores vazios é zero. Se os comprimentos forem diferentes, lança
ValueError. Usa zip depois de validar a condição. Para [2, 3] e [4, 5],
o resultado é .
def produto_interno(u, v):
return 0Verifica o código para comparar o resultado com os testes.
Testes
Produtos correspondentes
Código do testeprint(produto_interno([2, 3], [4, 5]))Esperado:23
Sinais e zero
Código do testeprint(produto_interno([-2, 0, 3], [4, 7, -1]))Esperado:-11
Vetores vazios
Código do testeprint(produto_interno([], []))Esperado:0
Comprimentos diferentes
Código do testetry: produto_interno([1, 2], [3]) except ValueError: print("comprimentos diferentes") else: print("erro não detetado")Esperado:comprimentos diferentes
print(produto_interno([2, 3], [4, 5]))23
print(produto_interno([-2, 0, 3], [4, 7, -1]))-11
print(produto_interno([], []))0
try:
produto_interno([1, 2], [3])
except ValueError:
print("comprimentos diferentes")
else:
print("erro não detetado")comprimentos diferentes
Pista
zip termina no vetor mais curto e oculta dados em falta.Solução
Primeiro comparamos os comprimentos. Só depois percorremos os pares e acumulamos cada produto. O total inicial zero também resolve a entrada vazia.
def produto_interno(u, v):
if len(u) != len(v):
raise ValueError("comprimentos diferentes")
total = 0
for a, b in zip(u, v):
total += a * b
return totalPara [-2, 0, 3] e [4, 7, -1], os produtos são -8, 0 e -3, cuja soma é -11.
Não somamos todos os pares possíveis: cada posição só combina com a posição
correspondente. O tratamento de raise é desenvolvido na lição de exceções.
Comparar dois nomes
a = [1, 2], b = a,
c = a.copy(). Depois de b[0] = 9, prevê
a, c, a is b e
a == c.
Pista
Mais uma pista
Solução
a vale [9, 2], c vale [1, 2],
a is b é True e a == c é
False. A mudança feita por b afeta a lista de a, sem mudar c.
Confere a tua resposta:
- Previ [9, 2] para a.
- Previ [1, 2] para c.
- Distingui
Truena identidade eFalsena igualdade.
Erros frequentes
Evitar linhas partilhadas
Corrige zeros(linhas, colunas) para devolver uma matriz de zeros com linhas
independentes. As dimensões são inteiros não negativos; não imprimas.
def zeros(linhas, colunas):
return [[0] * colunas] * linhasVerifica o código para comparar o resultado com os testes.
Testes
Alterar só a primeira linha
Código do testem = zeros(3, 2) m[0][0] = 5 print(m)Esperado:[[5, 0], [0, 0], [0, 0]]
Sem linhas
Código do testeprint(zeros(0, 3))Esperado:[]
Sem colunas
Código do testeprint(zeros(2, 0))Esperado:[[], []]
Linhas vazias independentes
Código do testem = zeros(2, 0) m[0].append(5) print(m)Esperado:[[5], []]
Uma célula
Código do testeprint(zeros(1, 1))Esperado:[[0]]
m = zeros(3, 2)
m[0][0] = 5
print(m)[[5, 0], [0, 0], [0, 0]]
print(zeros(0, 3))[]
print(zeros(2, 0))[[], []]
m = zeros(2, 0)
m[0].append(5)
print(m)[[5], []]
print(zeros(1, 1))[[0]]
Pista
A repetição exterior repete referências à mesma linha, não a copia.
Mais uma pista
Cria uma lista nova em cada iteração, mesmo quando há zero colunas.
Solução
def zeros(linhas, colunas):
matriz = []
for i in range(linhas):
matriz.append([0] * colunas)
return matrizCada iteração cria outra linha. Com zero linhas, obténs []; com duas linhas
e zero colunas, obténs duas listas vazias distintas. Alterar uma delas não
afeta a outra.
Erros frequentes
Repetir a mesma lista interior, copiar só a lista exterior ou usar print em
vez de return.
Verificar um contrato
list(zip([1, 2, 3], [4, 5])) produz o quê?
None.Sem strict=True, não rejeita estes comprimentos.Pista
zip termina na sequência mais curta por defeito.
Mais uma pista
Solução
Produz [(1, 4), (2, 5)]. O elemento 3 não tem correspondente.
Para um contrato de comprimentos iguais, verifica-os explicitamente.
[(1, 4), (2, 5)]. Só existem dois pares completos.[(1, 4), (2, 5), (3, None)]. zip não preenche a sequência menor comNone.ValueError. Semstrict=True, não rejeita estes comprimentos.
Erros frequentes
Assumir que zip rejeita sempre comprimentos diferentes.
Guardar uma ordenação
xs = [3, 1, 2]. Se executares ys = xs.sort(),
quanto vale ys?
None.Este é o novo conteúdo de xs, não o retorno.A entrada foi alterada.Pista
Mais uma pista
Solução
ys vale None e xs passa a [1, 2, 3]. Para
receber outra lista, usa ys = sorted(xs).
None. sort altera a lista e devolveNone.[1, 2, 3]. Este é o novo conteúdo de xs, não o retorno.[3, 1, 2]. A entrada foi alterada.
Erros frequentes