Se você já tentou copiar uma lista ou dicionário em Python e acabou modificando os dois sem querer, este guia é para você. O comportamento de cópia em Python é uma das fontes mais comuns de bugs sutis, especialmente para quem está começando. Entender a diferença entre cópia rasa (shallow copy) e cópia profunda (deep copy) é essencial para escrever código previsível e livre de efeitos colaterais.
Neste guia completo, você vai aprender desde o conceito de atribuição vs cópia até técnicas avançadas com o módulo copy da biblioteca padrão. Vamos explorar exemplos práticos com listas, dicionários, objetos aninhados e classes personalizadas, além de discutir performance e boas práticas.
Atribuição Não é Cópia
Antes de mergulharmos nos tipos de cópia, é fundamental entender que atribuir uma variável a outra não cria uma cópia. Em Python, variáveis são rótulos (referências) para objetos na memória. Quando você faz b = a, você está apenas criando um novo rótulo para o mesmo objeto. A documentação oficial do Python sobre atribuição explica que o operador = vincula o nome ao objeto, sem duplicá-lo.
# Atribuição comum — não é cópia
lista_original = [1, 2, [3, 4]]
lista_referencia = lista_original
lista_referencia.append(5)
print(lista_original) # [1, 2, [3, 4], 5] — a original foi alterada!
Esse comportamento ocorre porque lista_referencia e lista_original apontam para o mesmo objeto na memória. Qualquer modificação feita por uma referência afeta a outra. Esse é o conceito fundamental que motiva a necessidade de métodos de cópia.
O Que é Shallow Copy (Cópia Rasa)?
A shallow copy (cópia rasa) cria um novo objeto, mas não duplica os objetos internos. Em vez disso, ela copia as referências para os elementos contidos no objeto original. Isso significa que o objeto de nível superior é novo, mas os objetos aninhados (como listas dentro de listas) continuam sendo compartilhados entre a cópia e o original.
O Python oferece várias formas de criar shallow copies. A mais comum é usando o módulo copy:
import copy
lista_original = [1, 2, [3, 4]]
lista_copia = copy.copy(lista_original) # shallow copy
lista_copia.append(5) # só afeta a cópia
lista_copia[2].append(5) # afeta AMBAS! Objeto interno é compartilhado
print(lista_original) # [1, 2, [3, 4, 5]]
print(lista_copia) # [1, 2, [3, 4, 5], 5]
Perceba que append(5) no nível superior só modificou a cópia, mas append(5) na lista aninhada modificou ambos. Isso porque a lista interna [3, 4] não foi duplicada — apenas a referência para ela foi copiada. A documentação oficial do módulo copy descreve esse comportamento em detalhes.
Métodos de Shallow Copy
Além de copy.copy(), existem outras maneiras de criar shallow copies em Python:
- Método
.copy()de listas e dicionários:lista.copy()edict.copy()criam shallow copies. - Fatiamento (
[:]):nova_lista = lista_original[:]cria uma shallow copy da lista. - Construtor
list()edict():list(original)edict(original)também criam shallow copies. - Módulo
copy:copy.copy(objeto)funciona com qualquer tipo de objeto.
Para quem trabalha com listas em Python, dominar essas técnicas é fundamental para evitar comportamentos inesperados. A Real Python possui um tutorial aprofundado sobre cópia de objetos em Python que vale a pena consultar.
O Que é Deep Copy (Cópia Profunda)?
A deep copy (cópia profunda) cria um novo objeto e, recursivamente, duplica todos os objetos aninhados encontrados. O resultado é uma cópia completamente independente do original, sem qualquer referência compartilhada. Qualquer modificação na cópia profunda não afeta o original, e vice-versa.
import copy
lista_original = [1, 2, [3, 4]]
lista_deep = copy.deepcopy(lista_original)
lista_deep.append(5)
lista_deep[2].append(5)
print(lista_original) # [1, 2, [3, 4]] — intacta!
print(lista_deep) # [1, 2, [3, 4, 5], 5]
A função copy.deepcopy() percorre toda a árvore de objetos e cria duplicatas de cada nó encontrado. Isso garante isolamento total entre a cópia e o original, mas tem um custo computacional mais alto, especialmente para estruturas grandes ou que contêm muitos níveis de aninhamento. A discussão no Stack Overflow sobre o assunto é uma das mais votadas da plataforma, mostrando como esse tema é relevante.
Como o deepcopy Funciona Internamente
O copy.deepcopy() usa um dicionário interno de "memória" (memo) para rastrear objetos já copiados. Isso é essencial para evitar loops infinitos em estruturas com referências circulares e para garantir que objetos compartilhados não sejam duplicados múltiplas vezes. Se você está estudando dicionários em Python, entender deep copy é fundamental, já que dicionários com valores aninhados são um dos casos de uso mais comuns.
O mecanismo pode ser resumido em três etapas:
- Verificação do memo: Se o objeto já foi copiado, retorna a cópia existente.
- Criação do novo objeto: Usa
__deepcopy__ou__copy__se definidos, ou cria um novo objeto vazio do mesmo tipo. - Cópia recursiva: Para cada atributo ou elemento, chama
deepcopyrecursivamente e armazena no novo objeto.
A PEP 8 — Guia de Estilo Python recomenda que métodos de cópia sejam implementados de forma explícita nas classes que precisam de comportamento personalizado.
Shallow Copy vs Deep Copy: Comparação Direta
Vamos consolidar as diferenças em uma tabela comparativa para facilitar a consulta rápida:
| Característica | Shallow Copy | Deep Copy |
|---|---|---|
| Cria novo objeto de topo? | Sim | Sim |
| Copia objetos aninhados? | Não (apenas referências) | Sim (recursivamente) |
| Performance | Rápida | Mais lenta |
| Uso de memória | Baixo | Alto |
| Isolamento total? | Não | Sim |
| Funciona com objetos personalizados? | Sim (padrão) | Sim (padrão) |
| Suporta referências circulares? | Não causa problema | Sim (via memo) |
Cópia em Diferentes Estruturas de Dados
Cópia de Listas
Listas são a estrutura mais comum onde a diferença entre shallow e deep copy aparece. Veja exemplos com diferentes níveis de aninhamento:
import copy
Lista simples (sem aninhamento)
simples = [1, 2, 3]
s_shallow = copy.copy(simples)
s_deep = copy.deepcopy(simples)
Ambos funcionam de forma equivalente aqui, pois não há objetos aninhados
Lista aninhada
aninhada = [[1, 2], [3, 4]]
a_shallow = copy.copy(aninhada)
a_deep = copy.deepcopy(aninhada)
aninhada[0].append(99)
print(aninhada) # [[1, 2, 99], [3, 4]]
print(a_shallow) # [[1, 2, 99], [3, 4]] — afetado!
print(a_deep) # [[1, 2], [3, 4]] — isolado
Cópia de Dicionários
import copy
dados = {"nome": "Maria", "endereco": {"cidade": "SP", "cep": "01001"}}
copia_rasa = copy.copy(dados)
copia_profunda = copy.deepcopy(dados)
copia_rasa["endereco"]["cidade"] = "RJ"
print(dados["endereco"]["cidade"]) # RJ — shallow copy compartilha o dict interno
copia_profunda["endereco"]["cidade"] = "BH"
print(dados["endereco"]["cidade"]) # RJ — deep copy manteve o original intacto
Dicionários com valores aninhados (como o campo "endereco" acima) são a situação mais comum onde shallow copy causa surpresas. Sempre que seu dicionário contiver outros dicionários, listas ou objetos como valores, você deve considerar usar deep copy se precisar de isolamento completo.
Cópia de Objetos Personalizados
Para classes que você mesmo cria, o comportamento de cópia pode ser personalizado implementando os métodos __copy__ e __deepcopy__:
import copy
class Endereco:
def init(self, cidade, cep):
self.cidade = cidade
self.cep = cep
class Pessoa:
def init(self, nome, endereco):
self.nome = nome
self.endereco = endereco
def __copy__(self):
# Shallow copy personalizada
return Pessoa(self.nome, self.endereco)
def __deepcopy__(self, memo):
# Deep copy personalizada
return Pessoa(
copy.deepcopy(self.nome, memo),
copy.deepcopy(self.endereco, memo)
)
end = Endereco("SP", "01001")
p1 = Pessoa("João", end)
p2 = copy.deepcopy(p1)
p2.endereco.cidade = "RJ"
print(p1.endereco.cidade) # SP — isolado graças ao deepcopy
A documentação do método __deepcopy__ explica que o parâmetro memo é um dicionário que rastreia objetos já copiados, evitando duplicação e loops infinitos.
Quando Usar Cada Tipo de Cópia?
Escolher entre shallow copy e deep copy depende do seu caso de uso. Aqui estão diretrizes práticas:
Prefira Shallow Copy Quando:
- Os objetos contidos são imutáveis (ints, strings, tuplas sem objetos mutáveis).
- Você quer eficiência e a estrutura tem apenas um nível de profundidade.
- Você intencionalmente quer compartilhar objetos internos (ex: um cache compartilhado).
- Está trabalhando com grandes volumes de dados onde deep copy seria muito caro.
Prefira Deep Copy Quando:
- Há objetos mutáveis aninhados em múltiplos níveis.
- Você precisa de isolamento completo entre original e cópia.
- Está implementando padrões como snapshot de estado ou rollback.
- Você não tem controle sobre quem vai modificar os objetos internos.
Performance e Boas Práticas
A deep copy é significativamente mais custosa que a shallow copy, tanto em tempo de execução quanto em uso de memória. Para objetos grandes, a diferença pode ser de ordens de magnitude. Se você está processando gigabytes de dados, uma deep copy pode facilmente consumir toda a memória disponível.
Algumas boas práticas para trabalhar com cópia em Python:
- Documente o comportamento: Se sua classe implementa
__copy__ou__deepcopy__, documente o que é copiado rasa ou profundamente. - Prefira imutabilidade: Use tuplas, frozensets e tipos imutáveis sempre que possível. Eles nunca precisam ser copiados profundamente.
- Considere alternativas: Em vez de copiar, às vezes é melhor projetar seu código para não precisar de cópia (ex: usando factory functions).
- Cuidado com deepcopy em objetos complexos: Objetos com conexões de rede, arquivos abertos ou locks não devem ser copiados com deepcopy.
A documentação oficial do deepcopy alerta que objetos como módulos, classes, funções, tipos de máquina, arquivos e generators não podem ser copiados. O módulo pickle é uma alternativa para serialização e cópia de objetos em alguns cenários.
Casos Avançados
Referências Circulares
Estruturas com referências circulares (um objeto que contém uma referência para si mesmo) são tratadas corretamente pelo deepcopy graças ao parâmetro memo:
import copy
class Node:
def init(self, valor):
self.valor = valor
self.proximo = None
a = Node(1)
b = Node(2)
a.proximo = b
b.proximo = a # referência circular!
copia = copy.deepcopy(a) # funciona sem problemas
print(copia.proximo.proximo.valor) # 1
Cópia com Filtros
Você pode controlar quais atributos são copiados implementando __deepcopy__ de forma seletiva:
import copy
class Config:
def init(self):
self.dados = {"chave": "valor"}
self.cache = {"temp": "dados"} # não queremos copiar isso
def __deepcopy__(self, memo):
novo = Config()
novo.dados = copy.deepcopy(self.dados, memo)
# cache não é copiado — será recriado vazio
return novo
Erros Comuns e Como Evitá-los
Aqui estão os erros mais frequentes ao trabalhar com cópia em Python e como evitá-los:
Erro #1: Assumir que = cria uma cópia
Como vimos no início, b = a cria uma nova referência, não uma cópia. Sempre use copy() ou deepcopy() quando precisar de independência.
Erro #2: Usar copy.copy() em estruturas profundamente aninhadas
Se sua estrutura tem objetos mutáveis em múltiplos níveis, shallow copy não é suficiente. Use deepcopy() ou repense o design.
Erro #3: Aplicar deepcopy indiscriminadamente
Deep copy é cara. Use-a apenas quando necessário. Para objetos simples ou imutáveis, shallow copy é suficiente e muito mais eficiente. O artigo do GeeksForGeeks sobre copy em Python mostra exemplos práticos dessa diferença de performance.
Erro #4: Ignorar objetos não copiáveis
Nem todos os objetos podem ser copiados com deepcopy. Conexões de banco, sockets, threads e objetos de sistema operacional falham. Sempre verifique a documentação da biblioteca que você está usando. A documentação oficial de estruturas de dados do Python é um excelente ponto de partida para entender a fundo esses conceitos.
Conclusão
Dominar a diferença entre copy e deep copy em Python é uma habilidade essencial para qualquer desenvolvedor que trabalha com estruturas de dados mutáveis. A escolha correta entre cópia rasa e profunda pode evitar bugs difíceis de encontrar e melhorar significativamente a previsibilidade do seu código.
A regra de ouro é simples: use shallow copy para objetos planos ou quando você sabe que os objetos internos são imutáveis; use deep copy quando houver objetos mutáveis aninhados e você precisar de isolamento completo. E, acima de tudo, lembre-se de que atribuição (=) nunca é cópia.
Continue explorando o Universo Python com nossos guias gratuitos sobre listas em Python e dicionários em Python para aprofundar seu conhecimento em manipulação de dados na linguagem.