quarta-feira, 9 de novembro de 2011

Erros Comuns (6): copy vs deepcopy: aliasing e mutabilidade

Está na altura de introduzir um erro comum, mas cuja origem é mais subtil.
Sabemos que um mesmo objecto pode ter associado diferentes nomes. Quando os objectos são imutáveis isso não coloca problemas. No exemplo abaixo o objecto 'abc' tem associado três nomes diferentes. Se alteramos algum deles o que acontece é que é criado um novo objecto que fica associado ao nome do objecto antigo. Lembre-se: as cadeias de caracteres são imutáveis!



>>> cadeia_1 = 'abc'
>>> cadeia_2 = 'abc'
>>> cadeia_3 = cadeia_1
>>> id(cadeia_1)
4357502896
>>> id(cadeia_2)
4357502896
>>> id(cadeia_3)
4357502896
>>> cadeia_2 = cadeia_2 + 'd'
>>> cadeia_3 = cadeia_3 + '3'
>>> cadeia_2
'abcd'
>>> cadeia_3
'abc3'
>>> cadeia_1
'abc'
>>> id(cadeia_2)
4361446864
>>> id(cadeia_3)
4361446720
>>> id(cadeia_1)
4357502896


Quando usamos objectos mutáveis o caso muda de figura.


>>> lista_1 = [1,2,3]
>>> lista_2 = [1,2,3]
>>> lista_3 = lista_2
>>> lista_1
[1, 2, 3]
>>> lista_2
[1, 2, 3]
>>> lista_3
[1, 2, 3]
>>> id(lista_1)
4515141032
>>> id(lista_2)
4515145344
>>> id(lista_3)
4515145344
>>> lista_1[1] = 'b'
>>> lista_1
[1, 'b', 3]
>>> lista_2
[1, 2, 3]
>>> lista_3
[1, 2, 3]
>>> lista_2[1] = 'X'
>>> lista_1
[1, 'b', 3]
>>> lista_2
[1, 'X', 3]
>>> lista_3
[1, 'X', 3]
>>>


Para melhor entender o que acontece vejamos a figura seguinte:




Como se pode ver a lista_2 e a lista_3 partilham a memória, e é isso que faz com que as modificações que afectam uma também afectem a outra. Um modo de resolver o problema consiste em fazer uma cópia do objecto em vez de partilharem a memória.


>>> lista_1 = [1,2,3]
>>> lista_2 = [1,2,3]
>>> lista_3 = lista_2[:] # <-- cópia!
>>> id(lista_1)
4515141536
>>> id(lista_2)
4515105464
>>> id(lista_3)
4515140816
>>> lista_3[1] = 'Y'
>>> lista_3
[1, 'Y', 3]
>>> lista_2
[1, 2, 3]
>>>


A figura abaixo ilustra a situação.





Parece que podemos ficar descansados com esta solução. Mas veja-se uma nova situação.


>>> lista_2 = [1,[2],3]
>>> lista_3 = lista_2[:] # <-- cópia?
>>> id(lista_2)
4515346624
>>> id(lista_3)
4515144696
>>> lista_3[1][0] = 'Z'
>>> lista_3
[1, ['Z'], 3]
>>> lista_2
[1, ['Z'], 3] # oops!
>>>


O que terá acontecido? Para entender vamos recorrer uma vez mais ao nosso diagrama de ligação dos nomes aos objectos. Como fica situação antes da alteração?





E depois da alteração da lista_3?






O método indicado de cópia apenas faz uma cópia de superfície, isto é, uma cópia ao primeiro nível. A única solução efectiva para esta questão é recorrer à função deepcopy do módulo copy.


>>> import copy
>>> lista_2 = [1,[2],3]
>>> lista_3 = copy.deepcopy(lista_2) #<-- cópia profunda!
>>> id(lista_2)
4515145344
>>> id(lista_3)
4515140816
>>> lista_3[1][0] = 'K'
>>> lista_3
[1, ['K'], 3]
>>> lista_2
[1, [2], 3] # <-- Sem problemas!
>>>


Vejamos graficamente o que acontece.





Agora nada é partilhado, a não ser os objectos primitivos (imutáveis). Por isso ao alteramos a ligação (profunda) na lista_3 nada se altera na lista_2.

Sem comentários:

Enviar um comentário