Você sabe copiar objetos em Python?
Preste atenção no código abaixo. Nele, primeiro atribuímos uma lista à variável a. Depois, igualamos uma nova variável b a a.
a = [1, 2, 3, 4, 5]
b = a
Veja que ambas as listas são de fato iguais:
print('a: ', a)
print('b: ', b)
# resultados
# a: [1, 2, 3, 4, 5]
# b: [1, 2, 3, 4, 5]
Em seguida, adicionamos um novo elemento à lista b utilizando o método append(). Note o que ocorre com a variável a:
b.append(10)
print('a: ', a)
print('b: ', b)
# resultados
# a: [1, 2, 3, 4, 5, 10]
# b: [1, 2, 3, 4, 5, 10]
Mudamos b e isso causou uma alteração idêntica em a. Se você achou esse resultado estranho, este post é para você.
Identidade e localização de memória
No trecho de código a seguir, criamos duas variáveis x e y equivalentemente ao que foi feito acima. Observe que x aponta para a mesma localização de memória de y. Podemos ver isso aplicando a função id() em x e y:
x = 10
y = x
print(id(x), id(y))
# resultados
# 9767048 9767048
Assim como ocorreu com as variáveis x e y, listas copiadas utilizando o sinal de ‘=’ apontam para a mesma localização de memória.
a = [1, 2, 3, 4, 5]
b = a
print(id(a), id(b))
# resultados: 127693393388800 127693393388800
Listas, assim como dicionários, são estruturas de dados mutáveis. Para objetos Python que não são estruturas de dados mutáveis como int, float e str, ao mudarmos seus valores, as variáveis recebem localizações de memória separadas. Podemos ilustrar isso mudando o valor de y do exemplo anterior:
y = 5
print(id(x), id(y))
# resultados
# 9767048 9766888
Porém, esse procedimento pode ser problemático quando lidamos com objetos que são estruturas de dados mutáveis como listas, dicionários e até arrays do NumPy.
Cópia de listas
Considere o trecho de código abaixo, onde atribuirmos uma lista a duas variáveis, nomes1 e nomes2:
nomes1 = ['Ana', 'Maria', 'Marcelo']
nomes2 = nomes1
print(id(nomes1), id(nomes2))
# resultados
# 136440308222400 136440308222400
Assim como ocorreu no exemplo com x e y mostrado anteriormente, nomes1 e nomes2 ocupam a mesma localização de memória. No entanto, diferentemente de x e y do exemplo anterior, nomes1 e nomes2 são listas, ou seja, são objetos que contêm objetos e isso faz toda a diferença. Um objeto lista em Python possui elementos com referências próprias, mas essas referências não fazem parte da lista em si.
Para entender isso melhor, veja a figura abaixo. Ela mostra que um objeto lista (em azul) possui referências a outros objetos. No nosso exemplo, o objeto lista, referenciado por ambas as variáveis nomes1 e nomes2, possui referências a três objetos de string (‘Ana’, ‘Maria’ e ‘Marcelo’). A lista possui os elementos que se referem a essas strings. Porém, as strings em si não pertencem à lista.
Como as strings não pertencem à lista, ao mudarmos um desses elementos, não estamos mudando as referências do objeto lista, mas a referência de um de seus elementos. Esse processo é ilustrado no exemplo abaixo. Neste exemplo, alteramos o elemento com índice 0 na lista nomes2. Como explicado acima, isso não altera a referência ao objeto lista. O que estamos alterando nesse exemplo é a referência do elemento de índice 0 contido no objeto lista. Por isso a lista nomes1 também é alterada:
nomes2[0] = 'Tiago'
print(nomes1, nomes2)
# resultados
# ['Tiago', 'Maria', 'Marcelo'] ['Tiago', 'Maria', 'Marcelo']
Além disso, a localização de memória das duas continua igual:
print(id(nomes1), id(nomes2))
# resultados
# 136440308222400 136440308222400
A razão para esse resultado é simples. Neste exemplo, como indicado na figura abaixo, não temos duas listas: só temos dois nomes para a mesma lista! Portanto, quando igualamos uma variável a outra com sinal de “=” não estamos criando duas variáveis. Estamos definindo dois nomes para a mesma variável.
Muito além de listas
Como mencionado acima, este tipo de resultado não ocorre apenas com listas. Outros objetos compostos mutáveis também exibem o mesmo comportamento. Portanto, espere observar o mesmo comportamento com dicionários:
meu_dic1 = {
'var1': 1,
'var2': 2,
'var3': 3,
}
# iguala meu_dic2 a meu_dic1
meu_dic2 = meu_dic1
print(meu_dic1, meu_dic2)
# resultados: {'var1': 1, 'var2': 2, 'var3': 3} {'var1': 1, 'var2': 2, 'var3': 3}
# modifica meu_dic2
meu_dic2['var1'] = 100
# imprime os dois dics para verificar que a mudança em meu_dic2 alterou meu_dic1
print(meu_dic1, meu_dic2)
# resultados: {'var1': 100, 'var2': 2, 'var3': 3} {'var1': 100, 'var2': 2, 'var3': 3}
Neste exemplo, criamos o meu_dic2 o igualando ao meu_dic1. Por causa disso, mudanças no meu_dic2 alteram o meu_dic1 também. O mesmo padrão também é observado com arrays NumPy:
import numpy as np
# cria um array NumPy arr1
arr1 = np.array([1, 2, 3, 4, 5])
# iguala arr2 a arr1
arr2 = arr1
# imprime os dois arrays
print(arr1, arr2)
# resultados: [1 2 3 4 5] [1 2 3 4 5]
# modifica arr2
arr2[0] = 10
# imprime os dois arrays
print(arr1, arr2)
# resultados: [10 2 3 4 5] [10 2 3 4 5]
Dataframes do pandas também sofrem do mesmo problema:
import pandas as pd
# dados de exemplos
dados = {
'var1': [1, 2],
'var2': [3, 4],
}
# converte dados para dataframe
df1 = pd.DataFrame(dados)
# iguala df2 a df1
df2 = df1
print('df1', '\n', df1, '\n')
print('df2', '\n', df2)
# resultados
# var1 var2
# 0 1 3
# 1 2 4
# df2
# var1 var2
# 0 1 3
# 1 2 4
# altera df2
df2.loc[0, 'var1'] = 100
# verifica resultado da mudança nos dois dataframes
print('df1', '\n', df1, '\n')
print('df2', '\n', df2)
# df1
# var1 var2
# 0 100 3
# 1 2 4
# df2
# var1 var2
# 0 100 3
# 1 2 4
o método copy()
Mas como evitar esse tipo de situação? Em Python, a classe lista (list) oferece o método copy() para casos em que precisamos copiar listas. O mesmo método pode ser utilizado para copiar arrays NumPy, dataframes, dicionários, etc. O uso do método copy() evita as alterações que vimos nos exemplos anteriores:
animais1 = ['gato', 'cachorro', 'rato']
animais2 = animais1.copy() # cria lista animais2 usando copy()
# modifica lista animais2
animais2[2] = 'elefante'
print(animais1, animais2)
# resultados
# ['gato', 'cachorro', 'rato'] ['gato', 'cachorro', 'elefante']
print(id(animais1), id(animais2))
# 127281319823808 127281319825664
No trecho de código acima, criamos a lista animais2 utilizando o método copy() para copiar a lista animais1. Posteriormente, ao alterarmos o elemento de índice 2 da lista animais2, podemos verificar que a lista animais1 continuou inalterada. Ambas as listas também possuem localizações de memória distintas.
O método copy() resolve os problemas mais comuns observados nos exemplos anteriores. Mas, mesmo com o uso do método copy(), estruturas de dados mutáveis como listas e dicionários ainda podem apresentar problemas inesperados durante a cópia. E a razão para isso pode ser entendida na descrição do método copy().
Cópia superficial
Se você buscar a descrição do método utilizando o help do Python, obterá a seguinte explicação:
help(list.copy)
# Help on method_descriptor:
# copy(self, /) unbound builtins.list method
# Return a shallow copy of the list.
Observe a última linha: ela informa que o método retorna uma shallow copy da lista. Isso significa que copy() retorna uma cópia superficial.
Um objeto lista em Python é um objeto consistindo em uma sequência ordenada de referências a objetos Python. Na figura abaixo temos uma lista de strings. O objeto lista é apenas a caixa azul com as setas. As setas são as referências às strings. As strings em si não fazem parte da lista.
Sabemos bem que objetos listas não precisam conter apenas strings. Listas podem conter vários tipos de objetos incluindo outras listas, tuplas, dicionários, etc.
Quando uma lista é copiada com o método copy(), realizamos uma cópia superficial e copiamos as suas referências. Essa cópia superficial, embora suficiente em muitos cenários, pode causar erros bastante problemáticos em certas situações. Mas quais situações? Especialmente situações em que temos estruturas de dados aninhadas como uma lista dentro de outra lista.
As limitações das cópias superficiais
Vamos entender esses erros através de um exemplo. Suponha que temos um cadastro de clientes onde armazenamos seus nomes e telefones fixos. A cliente Ana possui dois números de telefone: um de sua casa e um profissional. Armazenamos esses dados em uma lista dentro da lista cliente1:
cliente1 = ['Ana', ['123-2323', '123-3434']]
Ana é casada com Pedro e ambos trabalham juntos. Para armazenar seus dados, como ambos possuem os mesmos números de telefones fixos, copiamos as listas e atualizamos apenas o nome. Veja que a lista é copiada com o método copy():
cliente2 = cliente1.copy()
cliente2[0] = 'Pedro'
print(cliente1, cliente2)
# resultados: ['Ana', ['123-2323', '123-3434']] ['Pedro', ['123-2323', '123-3434']]
Agora pense num cenário no qual os dois se divorciam, mas mantêm a parceria profissional. Precisamos atualizar o número de telefone da casa de Ana em nosso cadastro:
cliente1[1][0] = '123-4545'
Essa simples atualização tem uma consequência perigosa: ela também altera o cadastro do cliente2, Pedro.
print(cliente1, cliente2)
# resultados: ['Ana', ['123-4545', '123-3434']] ['Pedro', ['123-4545', '123-3434']]
Entendendo o erro observado
Por que obtivemos esse erro mesmo criando a lista cliente2 utilizando o método copy()? Como mencionado acima, copy() realiza cópias superficiais. Portanto, ao utilizarmos esse método para criar o cliente2, copiamos apenas as suas referências. No entanto, ambos os clientes fazem referência ao mesmo objeto de lista:
Consequentemente, quando mudamos essa lista aninhada, a alteração fica visível em ambas as listas:
cópias profundas em Python
Mas como resolver esse problema? A solução é realizar uma cópia profunda utilizando o método deepcopy() da classe copy:
import copy
cliente1 = ['Ana', ['123-2323', '123-3434']]
# cria cliente2 usando deepcopy()
cliente2 = copy.deepcopy(cliente1)
cliente2[0] = 'Pedro'
print(cliente1, cliente2)
# resultados: ['Ana', ['123-2323', '123-3434']] ['Pedro', ['123-2323', '123-3434']]
# altera cliente1
cliente1[1][0] = '123-4545'
# imprime os dois clientes para verificar que a mudança em cliente1 manteve cliente2 inalterado
print(cliente1, cliente2)
# resultados: ['Ana', ['123-4545', '123-3434']] ['Pedro', ['123-2323', '123-3434']]
Além de listas, você pode utilizar deepcopy() para copiar arrays NumPy, dicionários e o que mais precisar. Lembre-se: seu uso não é necessário sempre. Em muitas situações o método copy() já é suficiente. Mas, para estruturas de dados mutáveis aninhadas ou cenários incertos, deepcopy() pode evitar surpresas problemáticas em seus códigos.