Python Mutável × Imutável
A Ana copiou a lista de compras para a Bia com compras_bia = compras_ana. A Bia acrescentou pão na lista dela, e o pão apareceu também na lista da Ana. Não é defeito do Python: é a regra de como ele guarda as coisas. Alguns objetos podem ser modificados depois de criados (as listas, os dicionários, os conjuntos): são os mutáveis. Outros nunca mudam (os números, os textos, as tuplas): são os imutáveis. Entender essa diferença explica de uma vez vários comportamentos que, à primeira vista, parecem mágica.
- Que uma variável é uma etiqueta colada num objeto, e não uma caixa com o valor dentro.
- A usar
iseid()para saber se duas etiquetas estão no mesmo objeto. - Quais tipos são mutáveis e quais são imutáveis.
- A diferença entre mudar um objeto e trocar a etiqueta de lugar.
- Por que um texto nunca muda, e o que o
.upper()faz de verdade. - A lista dentro da tupla, e o
+=que dá erro e muda a lista mesmo assim. - O que acontece quando você passa uma lista ou um número para uma função.
- Por que só objetos imutáveis servem de chave de dicionário.
A ideia: etiquetas, não caixas
Muitos cursos dizem que a variável é "uma caixa onde você guarda um valor". Para o Python, uma imagem melhor é a de uma etiqueta. O objeto (a lista, o número, o texto) fica guardado na memória, e a variável é uma etiqueta com um nome, colada nele. compras_ana = ["café", "leite"] quer dizer: "crie uma lista e cole nela a etiqueta compras_ana".
Um objeto pode ter várias etiquetas. compras_bia = compras_ana não copia a lista: cola uma segunda etiqueta na mesma lista. Agora pense em dois tipos de objeto:
- Um quadro branco: dá para apagar e escrever de novo. Quem olha para ele, por qualquer etiqueta, vê a mudança. São os mutáveis.
- Uma placa de metal gravada: não muda nunca. Se você quer outro texto, manda fazer outra placa e muda a etiqueta de lugar. A placa velha continua igual. São os imutáveis.
| Imutáveis (placa gravada) | Mutáveis (quadro branco) |
|---|---|
int, float (números) | list (listas) |
bool (True, False) e None | dict (dicionários) |
str (textos) | set (conjuntos) |
tuple (tuplas) | objetos das suas classes, em geral (aula Classes) |
frozenset (conjunto congelado) |
1. Duas etiquetas, uma lista
compras_ana = ["café", "leite"]
compras_bia = compras_ana
compras_bia.append("pão")
print(compras_ana)
print(compras_bia)
print(compras_ana is compras_bia)
print(id(compras_ana))
print(id(compras_bia))
$ python3 etiquetas.py ['café', 'leite', 'pão'] ['café', 'leite', 'pão'] True 140568262729088 140568262729088
- compras_bia = compras_ana
- Não cria lista nenhuma: cola a etiqueta
compras_biano mesmo objeto. - compras_bia.append("pão")
- Muda a lista. Como só existe uma lista, as duas etiquetas "veem" o pão.
- compras_ana is compras_bia
- O
is(aula is e in) pergunta: "as duas etiquetas estão no mesmo objeto?"True. - id(compras_ana)
- A função
id()devolve um número que identifica o objeto enquanto ele existir (no Python do Ubuntu, é o endereço dele na memória). Mesmoid, mesmo objeto.
Rodando o mesmo programa mais uma vez:
$ python3 etiquetas.py ['café', 'leite', 'pão'] ['café', 'leite', 'pão'] True 139972417019264 139972417019264
Os números do id mudaram: a cada execução, o objeto vai parar num lugar diferente da memória. No seu computador e no editor do guia, você vai ver outros números. O que importa não é o número, e sim a comparação: os dois id são iguais entre si. Por isso, no dia a dia, em vez de olhar números de id, use o is.
is não é ==
a = [1, 2, 3] b = [1, 2, 3] c = a print(a == b, a is b) print(a == c, a is c)
$ python3 is_igual.py True False True True
a e b são duas listas iguais (mesmo conteúdo: == dá True), mas são dois objetos (is dá False): mudar uma não muda a outra. Já a e c são duas etiquetas no mesmo objeto. Resumindo: == compara o conteúdo; is compara a identidade.
2. Números e textos: a placa gravada
Agora o mesmo jogo com um número:
preco = 10 outro = preco outro += 1 print(preco) print(outro) print(preco is outro)
$ python3 numeros.py 10 11 False
O preco continuou 10. Um número não pode ser modificado: o 10 é sempre 10. O outro += 1 não mexeu no 10; calculou um número novo, o 11, e mudou a etiqueta outro para ele. A etiqueta preco ficou onde estava. Por isso, depois, preco is outro é False.
Com textos é igual:
nome = "café" outro = nome outro += " expresso" print(nome) print(outro) maiusculo = nome.upper() print(nome) print(maiusculo)
$ python3 texto.py café café expresso café CAFÉ
Repare no .upper(): o nome continuou café, em minúsculas. Os métodos de texto (aula Métodos de Texto) nunca mudam o texto original; eles devolvem um texto novo. Se você não guardar o resultado numa variável, ele se perde. E tentar trocar uma letra direto dá erro:
nome = "ana" nome[0] = "A"
$ python3 texto_erro.py
Traceback (most recent call last):
File "/home/ana/texto_erro.py", line 2, in <module>
nome[0] = "A"
~~~~^^^
TypeError: 'str' object does not support item assignment
Linha por linha:
File "/home/ana/texto_erro.py", line 2: o erro foi na linha 2.nome[0] = "A", com~~~~^^^embaixo: os sinais marcam onome[0], a posição que o programa tentou trocar.TypeError: 'str' object does not support item assignment: "o objeto do tipostrnão aceita atribuição a um item". Em outras palavras: não dá para trocar um pedaço de um texto.
Missão: o programa quer deixar a primeira letra do nome em maiúscula, mas dá TypeError. Conserte criando um texto novo e mudando a etiqueta nome para ele (dica: "A" + nome[1:], como na aula Índices e Fatias, ou um método de texto).
nome = "ana" nome[0] = "A" print(nome)
nome = "ana" nome = "A" + nome[1:] print(nome)
3. Mudar o objeto × trocar a etiqueta
Com listas, existem os dois jeitos, e eles dão resultados diferentes:
ana = ["café"]
bia = ana
ana.append("leite") # muda a lista
print(bia, ana is bia)
ana = ana + ["pão"] # cria outra lista e troca a etiqueta
print(bia, ana is bia)
print(ana)
$ python3 mudar_trocar.py ['café', 'leite'] True ['café', 'leite'] False ['café', 'leite', 'pão']
ana.append("leite")mudou a lista. A Bia, que tem etiqueta na mesma lista, viu o leite.ana = ana + ["pão"]criou uma lista nova (o+sempre cria) e mudou a etiquetaanapara ela. A lista antiga, que a Bia continua olhando, não ganhou o pão. E agoraana is biaéFalse.
+= numa lista muda a lista
Você poderia esperar que ana += ["pão"] fosse igual a ana = ana + ["pão"]. Com números e textos é. Com listas, não: o += de uma lista muda a própria lista, como o .extend().
ana = ["café"] bia = ana ana += ["leite"] print(bia, ana is bia) x = 10 y = x x += 1 print(y, x is y)
$ python3 mais_igual.py ['café', 'leite'] True 10 False
Na lista, a Bia viu o leite (mesmo objeto). No número, o y continuou 10 (o x foi para um número novo). É a mesma escrita, +=, com efeitos diferentes, porque um tipo é mutável e o outro não.
4. A lista dentro da tupla
A tupla (aula Tuplas) é imutável: não dá para trocar os itens dela. Mas e se um dos itens for uma lista?
aluna = ("Bia", [8.5, 7.0])
aluna[1].append(9.0)
print(aluna)
aluna[0] = "Beatriz"
$ python3 tupla.py
('Bia', [8.5, 7.0, 9.0])
Traceback (most recent call last):
File "/home/ana/tupla.py", line 4, in <module>
aluna[0] = "Beatriz"
~~~~~^^^
TypeError: 'tuple' object does not support item assignment
A nota 9.0 entrou, e só depois veio o erro, ao tentar trocar o nome. O segredo está na imagem das etiquetas: a tupla não guarda a lista "dentro dela"; ela guarda quem são os seus itens, como uma ficha que diz "item 0: aquele texto; item 1: aquela lista". O que a tupla proíbe é trocar o que a ficha diz (aluna[0] = …). A lista do item 1 continua sendo um quadro branco, e escrever nele (.append) não muda a ficha da tupla.
Uma tupla é imutável na primeira camada: os itens são sempre os mesmos objetos. Se algum item é mutável, ele pode mudar. Uma tupla só é imutável "até o fundo" quando todos os itens também são imutáveis, como ("Bia", 8.5, 7.0).
O += que dá erro e muda mesmo assim
Juntando as duas coisas que você viu (o += de lista muda a lista; a tupla não deixa trocar itens), aparece um dos comportamentos mais curiosos do Python:
aluna = ("Bia", [8.5, 7.0])
try:
aluna[1] += [9.0]
except TypeError as e:
print("Erro:", e)
print(aluna)
$ python3 tupla_paradoxo.py
Erro: 'tuple' object does not support item assignment
('Bia', [8.5, 7.0, 9.0])
Deu erro e a nota entrou. O aluna[1] += [9.0] faz duas coisas, nesta ordem: (1) muda a lista com o += de lista, o que funciona; (2) tenta guardar o resultado de volta em aluna[1], o que a tupla proíbe, e aí vem o TypeError. A lição prática: para mexer numa lista que está dentro de uma tupla, use os métodos da lista (aluna[1].append(9.0)), que não tentam o passo 2.
5. Passando para uma função
Quando você chama uma função, o parâmetro vira mais uma etiqueta no mesmo objeto que você passou (aula Parâmetros e Argumentos). O resultado depende do tipo:
def colocar_pao(lista):
lista.append("pão")
def somar_um(numero):
numero += 1
compras = ["café"]
colocar_pao(compras)
print(compras)
estoque = 5
somar_um(estoque)
print(estoque)
$ python3 funcao.py ['café', 'pão'] 5
colocar_paorecebeu uma etiqueta na listacomprase mudou a lista. Quando a função terminou, o pão continuava lá.somar_umrecebeu uma etiqueta no número 5. Onumero += 1criou o 6 e mudou só a etiquetanumero, que é da função e some quando ela termina (aula Escopo). Oestoquelá fora continuou 5.
E se a função trocar a etiqueta de uma lista, em vez de mudar a lista?
def esvaziar(lista):
lista = []
def esvaziar_de_verdade(lista):
lista.clear()
compras = ["café", "leite"]
esvaziar(compras)
print(compras)
esvaziar_de_verdade(compras)
print(compras)
$ python3 funcao_troca.py ['café', 'leite'] []
lista = [] criou uma lista vazia nova e mudou a etiqueta da função para ela; a lista de fora nem ficou sabendo. Já lista.clear() esvaziou o próprio objeto. A regra geral: dentro da função, mudar o objeto é visível lá fora; trocar a etiqueta, não.
Uma função que muda a lista que recebeu pode pegar de surpresa quem a chamou. Quando a ideia é calcular algo novo, prefira devolver uma lista nova com return e deixar a original em paz. Quando a ideia é mesmo mudar (como colocar_pao), deixe isso claro no nome e na docstring. É o que o próprio Python faz: sorted(lista) devolve uma lista nova; lista.sort() muda a lista e devolve None.
Missão: a função com_desconto deveria devolver os preços com 10% de desconto, mas ela estraga a lista original (rode e veja os "Originais"). Reescreva a função para criar e devolver uma lista nova, sem mexer em precos.
def com_desconto(precos):
for i in range(len(precos)):
precos[i] = precos[i] * 0.9
return precos
originais = [10.0, 20.0]
novos = com_desconto(originais)
print("Originais:", originais)
print("Com desconto:", novos)
def com_desconto(precos):
novos = []
for preco in precos:
novos.append(preco * 0.9)
return novos
originais = [10.0, 20.0]
novos = com_desconto(originais)
print("Originais:", originais)
print("Com desconto:", novos)
6. Só imutáveis servem de chave
Um dicionário acha as chaves depressa porque calcula, para cada uma, um número de "etiqueta" chamado hash. Se a chave pudesse mudar depois de guardada, o número ficaria errado e o dicionário se perderia. Por isso, chaves de dicionário e itens de conjunto têm de ser imutáveis:
mapa = {(0, 0): "entrada", (2, 3): "caixa"}
print(mapa[(2, 3)])
mapa[[1, 1]] = "padaria"
$ python3 chaves.py
caixa
Traceback (most recent call last):
File "/home/ana/chaves.py", line 3, in <module>
mapa[[1, 1]] = "padaria"
~~~~^^^^^^^^
TypeError: cannot use 'list' as a dict key (unhashable type: 'list')
Tuplas funcionaram como chave (as posições (0, 0) e (2, 3) no mapa da loja). A lista [1, 1], não: "não dá para usar list como chave de dicionário (tipo sem hash)". E a tupla precisa ser imutável até o fundo:
vistos = set()
vistos.add(("Bia", [8.5, 7.0]))
$ python3 chave_tupla_lista.py
Traceback (most recent call last):
File "/home/ana/chave_tupla_lista.py", line 2, in <module>
vistos.add(("Bia", [8.5, 7.0]))
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
TypeError: cannot use 'tuple' as a set element (unhashable type: 'list')
A mensagem é precisa: "não dá para usar a tuple como item do conjunto (o tipo sem hash é list)". O problema é a lista lá dentro.
Para conferir de uma vez quais tipos servem de chave, dá para perguntar ao Python:
from collections.abc import Hashable
for valor in [10, 2.5, True, None, "café", (1, 2), frozenset({1}), [1, 2], {"a": 1}, {1, 2}]:
print(type(valor).__name__, isinstance(valor, Hashable))
$ python3 descobrir.py int True float True bool True NoneType True str True tuple True frozenset True list False dict False set False
Exatamente a tabela do começo da aula: os imutáveis servem de chave; os mutáveis, não.
Formas diferentes de evitar a surpresa
| Você quer… | Faça |
|---|---|
| Uma lista independente, que pode mudar sem afetar a outra | Uma cópia: compras_bia = compras_ana.copy() (aulas Copiando Listas e Cópia Rasa e Cópia Profunda) |
| Dados que ninguém deve mudar | Uma tupla em vez de uma lista |
| Uma função que calcula algo a partir de uma lista | Devolva uma lista nova (return), não mude a recebida |
| Saber se duas variáveis estão no mesmo objeto | a is b (não compare números de id() de cabeça) |
| Saber se duas variáveis têm o mesmo conteúdo | a == b |
Quando dá errado
nomes = nomes.sort(): a lista virou None
nomes = ["Caio", "Ana", "Bia"] nomes = nomes.sort() print(nomes)
$ python3 sort_none.py None
Nenhum erro, e a lista sumiu. O .sort() muda a lista (ela é mutável) e devolve None (aula O Valor None), avisando "não há nada novo para guardar". Ao escrever nomes = …, você colou a etiqueta nomes nesse None. Como consertar: nomes.sort() sozinho, ou nomes = sorted(nomes), que devolve uma lista nova.
nome.upper() sozinho não faz nada
nome = "ana" nome.upper() print(nome)
$ python3 upper_sem.py ana
O erro contrário ao anterior. Textos são imutáveis: o .upper() criou "ANA" e devolveu, mas ninguém guardou. Como consertar: nome = nome.upper(). Regra para lembrar: métodos de texto devolvem um texto novo (guarde o resultado); métodos de lista que mudam a lista (append, sort, clear) devolvem None (não guarde).
def adicionar(item, lista=[]):
lista.append(item)
return lista
print(adicionar("café"))
print(adicionar("leite"))
$ python3 padrao_mutavel.py ['café'] ['café', 'leite']
A segunda chamada deveria devolver só ['leite']. O valor padrão [] é criado uma vez, quando o def roda, e todas as chamadas sem lista recebem uma etiqueta nesse mesmo objeto, que vai acumulando. Como consertar: use None como padrão e crie a lista lá dentro: def adicionar(item, lista=None): e if lista is None: lista = [] (aula Valores Padrão). As dataclasses recusam esse padrão com um ValueError (aula Dataclasses).
Você viu os dois na aula: TypeError: 'str' object does not support item assignment (seção 2) e TypeError: 'tuple' object does not support item assignment (seção 4). Como consertar: crie um objeto novo e mude a etiqueta (nome = "A" + nome[1:]; aluna = ("Beatriz",) + aluna[1:]), ou use uma lista, se o dado precisa mudar.
TypeError: cannot use 'list' as a dict key (unhashable type: 'list') (seção 6). Como consertar: converta a lista numa tupla: mapa[tuple(posicao)] = "padaria".
Referência rápida
| Escrita | O que faz |
|---|---|
b = a | Cola mais uma etiqueta no mesmo objeto (não copia) |
a is b | As duas etiquetas estão no mesmo objeto? |
a == b | Os dois objetos têm o mesmo conteúdo? |
id(a) | Número que identifica o objeto (muda a cada execução) |
lista.append(x), lista += […], lista.sort() | Mudam a lista (todas as etiquetas veem) |
lista = lista + […], sorted(lista) | Criam uma lista nova |
texto.upper(), texto.replace(…) | Devolvem um texto novo; o original não muda |
numero += 1, texto += "…" | Criam um objeto novo e mudam a etiqueta |
lista.copy() | Uma lista nova, com os mesmos itens |
| Sintoma | Causa provável |
|---|---|
| Mudei uma lista e a outra mudou junto | As duas variáveis são etiquetas no mesmo objeto (b = a): faça uma cópia |
| A função mudou a minha lista | Ela recebeu uma etiqueta na sua lista e a modificou |
'str' object does not support item assignment | Tentou trocar uma letra de um texto |
'tuple' object does not support item assignment | Tentou trocar um item de uma tupla (mesmo com +=) |
unhashable type: 'list' | Lista (ou tupla com lista dentro) como chave ou item de conjunto |
A variável virou None | Guardou o resultado de um método que muda a lista (x = x.sort()) |
| O texto não mudou | Chamou um método de texto sem guardar o resultado |
Exercícios
1. Mesmo objeto?
Complete o operador que pergunta se a e b são o mesmo objeto:
2. O que sai?
x = [1, 2], depois y = x, depois y.append(3). O que mostra print(len(x))?
3. Qual destes tipos é mutável?
4. a = 5, b = a, b += 1. Quanto vale a?
5. t = ("Bia", [8.5]) e depois t[1].append(9.0). O que acontece?
6. Por que {[1, 2]: "x"} dá erro?
Missão final: a lista da Bia
Missão: a Bia quer começar a lista dela a partir da lista da Ana e acrescentar pão, mas o pão está aparecendo na lista da Ana também. Mude uma linha para a Bia ganhar uma lista própria, com os mesmos itens.
compras_ana = ["café", "leite"]
compras_bia = compras_ana
compras_bia.append("pão")
print("Ana:", compras_ana)
print("Bia:", compras_bia)
compras_ana = ["café", "leite"]
compras_bia = compras_ana.copy()
compras_bia.append("pão")
print("Ana:", compras_ana)
print("Bia:", compras_bia)
Com a solução, o lab mostrou:
$ python3 main.py Ana: ['café', 'leite'] Bia: ['café', 'leite', 'pão']
A cópia resolve aqui porque os itens são textos, que são imutáveis. Se os itens fossem listas (a lista da turma, com as notas de cada aluno), a cópia simples não bastaria: é o assunto da próxima aula, Cópia Rasa e Cópia Profunda.
Resumo
- Variável é uma etiqueta num objeto.
b = anão copia: cola mais uma etiqueta no mesmo objeto. iscompara identidade (mesmo objeto);==compara conteúdo. O número doid()muda a cada execução: compare comis.- Imutáveis (números, textos, tuplas,
bool,None,frozenset) nunca mudam: toda "mudança" cria um objeto novo e troca a etiqueta. Mutáveis (listas, dicionários, conjuntos) mudam no lugar, e todas as etiquetas veem. - Métodos de texto devolvem um texto novo; métodos de lista que mudam a lista devolvem
None. - Uma tupla com uma lista dentro pode ter a lista modificada;
tupla[1] += [...]muda a lista e dáTypeError. - Numa função, mudar o objeto recebido é visível lá fora; trocar a etiqueta (
lista = []), não. - Só objetos imutáveis (até o fundo) servem de chave de dicionário ou item de conjunto.