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.

Você vai aprender
  • Que uma variável é uma etiqueta colada num objeto, e não uma caixa com o valor dentro.
  • A usar is e id() 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 Nonedict (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_bia no 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). Mesmo id, 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 o nome[0], a posição que o programa tentou trocar.
  • TypeError: 'str' object does not support item assignment: "o objeto do tipo str nã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 etiqueta ana para ela. A lista antiga, que a Bia continua olhando, não ganhou o pão. E agora ana is bia é False.
O += 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.

Imutável não quer dizer "congelado por inteiro"

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_pao recebeu uma etiqueta na lista compras e mudou a lista. Quando a função terminou, o pão continuava lá.
  • somar_um recebeu uma etiqueta no número 5. O numero += 1 criou o 6 e mudou só a etiqueta numero, que é da função e some quando ela termina (aula Escopo). O estoque lá 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.

Boa prática: não mude o que você recebeu sem avisar

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 outraUma cópia: compras_bia = compras_ana.copy() (aulas Copiando Listas e Cópia Rasa e Cópia Profunda)
Dados que ninguém deve mudarUma tupla em vez de uma lista
Uma função que calcula algo a partir de uma listaDevolva uma lista nova (return), não mude a recebida
Saber se duas variáveis estão no mesmo objetoa is b (não compare números de id() de cabeça)
Saber se duas variáveis têm o mesmo conteúdoa == 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).

A lista padrão que lembra das chamadas anteriores
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).

Trocar um item de texto ou de tupla

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.

Lista como chave de dicionário ou item de conjunto

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

EscritaO que faz
b = aCola mais uma etiqueta no mesmo objeto (não copia)
a is bAs duas etiquetas estão no mesmo objeto?
a == bOs 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
SintomaCausa provável
Mudei uma lista e a outra mudou juntoAs duas variáveis são etiquetas no mesmo objeto (b = a): faça uma cópia
A função mudou a minha listaEla recebeu uma etiqueta na sua lista e a modificou
'str' object does not support item assignmentTentou trocar uma letra de um texto
'tuple' object does not support item assignmentTentou 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 NoneGuardou o resultado de um método que muda a lista (x = x.sort())
O texto não mudouChamou 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:

print(a b)

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 = a não copia: cola mais uma etiqueta no mesmo objeto.
  • is compara identidade (mesmo objeto); == compara conteúdo. O número do id() muda a cada execução: compare com is.
  • 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.