shallow copy and deep copy in python

깊게 복사했나요 얕게 복사했나요.

  • 파이썬에서 가장 많이 쓰이는 컨테이너는 당연히 []{}입니다. 리스트와 딕셔너리죠. 자주 쓰이는 만큼, 사실 아무렇게나 쓰이는 것일지 몰라요.
  • 제 이름은 ‘이승훈’입니다. 또 제 아이디는 ‘frhyme’이죠. 이승훈도 frhyme도 모두 저를 가르키는 말입니다. 이건 shallow copy에요. 이 둘이 가리키는 객체가 서로 다르지 않거든요. 이름만 다를 뿐입니다.
  • 반대로, 누군가, ‘이승훈’이 가리키는 대상을 그대로 복사해서 미국에 ‘이승훈1’을 만들었다고 합시다. 이때 원래 이승훈을 바뀌었다고 해서 이승훈1이 바뀌지는 않아요. 이런 경우를 deep copy라고 합니다.

type in python

  • 우선 하나 정리해봅시다.

everything is object in python

  • 파이썬에서는 모든 것이 객체입니다. 심지어 Number(integer, float-point), string까지 모두 객체입니다.
for x in [1, 1.0, "sdd"]:
    print(f"{x}, Type: {type(x)},  id: {id(x)}, Is instance of object?? {isinstance(x, object)}")
1, Type: <class 'int'>,  id: 4422870368, Is instance of object?? True
1.0, Type: <class 'float'>,  id: 4470792984, Is instance of object?? True
sdd, Type: <class 'str'>,  id: 4470763624, Is instance of object?? True
  • ‘모든 것이 객체다’라는 말을 다시 곱씹어봅시다. 객체는 보통 여러 기본형(primitive data type)을 참고하여 만드는 혼합형 데이터죠. 여러 데이터를 복잡하게 참고하고 있기 때문에 대부분의 경우 메모리에 값을 저장하는 것이 아니라, 값이 저장된 메모리주소가 저장됩니다.
  • 이 말인 즉슨 =(assignment operator)를 사용해서 복사를 해주면, 값이 복사되는 것이 아니라, 메모리주소가 복사된다는 이야깁니다. 두 놈이 같은 실체를 가리키고 있는 상황(shallow copy)가 발생하기 쉽다는 것이죠.
  • 모든것이 객체인 파이썬에서는 오히려 default가 shallow copy라고 할 수 있습니다. 여기서는 value type과 reference type에 대한 개념을 잡고 가는 것이 필요할 수 있습니다.

shallow copy and deep copy in python

  • 일반적으로 어떤 변수에 값을 넣을때 우리는 =(assignment operator)를 사용합니다. 이 operator는 container나 객체를 넣을 때는 기본적으로 항상 shallow copy를 지원한다고 보면 되요.
  • 다음 코드처럼 리스트에 대해서 =를 넣어주면 shallow copy, .copy로 넣어주면 deep copy가 됩니다. 딕셔너리로, 리스트도 모두 마찬가지에요.
    • python의 built-in object들은 모두 기본적으로 .copy를 지원합니다.
    • 뿐만 아니라. pandas, numpy등 주요 라이브러리들도 .copy를 통해서 deep copy를 지원합니다.
print("lst case:")
lst_a = [i for i in range(0, 10)]
lst_b = lst_a
print(f"shallow copy: {id(lst_a)}, {id(lst_b)}")## id가 같으므로 shallow copy
lst_b = lst_a.copy()
print(f"   deep copy: {id(lst_a)}, {id(lst_b)}")## .copy를 이용했으므로 deep copy
print("="*21)
print("dictionary case:")
dict_a = {i:chr(i) for i in range(0, 10)}
dict_b = dict_a
print(f"shallow copy: {id(dict_a)}, {id(dict_b)}")
dict_b = dict_a.copy()
print(f"   deep copy: {id(dict_a)}, {id(dict_b)}")
print("="*21)
lst case:
shallow copy: 4470666248, 4470666248
   deep copy: 4470666248, 4470013192
=====================
dictionary case:
shallow copy: 4470570368, 4470570368
   deep copy: 4470570368, 4470870952
=====================

customized class deep copy

  • 그렇다면, 제가 직접 만든, class에서는 어떻게 deep copy를 지원할 수 있을까요?
  • 기본적으로, 앞에서 다 한 이야기들이지만 = assignment operator를 사용하면, shallow copy가 됩니다.
    • 클래스는 물론이고, 클래스의 attribute들인 객체들도 마찬가지죠.
class AA(object):
    def __init__(self, lst, dct):
        self.lst = lst
        self.dct = dct

lst1 = [i for i in range(0, 10)]
dct1 = {i:chr(i) for i in range(0, 10)}
a1 = AA(lst1, dct1)
a2 = a1
a3 = AA(lst1, dct1)
## a1과 a2가 같은 객체와 binding됨. 
print(id(a1), id(a2), id(a3))
## 또한, a1과 a3가 메모리 주소가 달라서 다를 것으로 알기 쉽지만, 내부 변수의 메모리 주소는 같음 
## 즉, shallow copy된 것 
print(id(a1.lst), id(a2.lst), id(a3.lst))
4470976864 4470976864 4470976920
4470748424 4470748424 4470748424

create method ‘copy’

  • 내부에 copy method를 만들어줍니다. 저는 python에서 기본적으로 모든 Object에 대해서 deepcopy method를 만들고 이를 기본 object에 포함하여, 만약 python의 클래스가 object라는 클래스를 상속받는다면 모든 경우에 대해서 deep copy method를 사용할 수 있도록 해줄 수 있을 것 같은데요. 왜 포함되어 있지 않은지 잘 모르겠습니다. 흠.
## 자 이제 어떻게 deep copy할까? 
## 1) 내부에 copy method를 만들어주자. 

class AA(object):
    def __init__(self, lst, dct):
        self.lst = lst
        self.dct = dct
    def copy(self):
        ## 모든 attribute에 대해서 copy해줘야 함
        return AA(self.lst.copy(), 
                  self.dct.copy())

a1 = AA(lst1, dct1)
a2 = a1.copy()
a3 = a1.copy()
print(id(a1), id(a2), id(a3))
print(id(a1.lst), id(a2.lst), id(a3.lst))
4470979160 4470979048 4470978320
4470748424 4470922440 4470968840

with copy module

  • 뭐, 이미 copy 라는 모듈이 있습니다. copy.deepcopy()를 사용하면 객체와 내부에 있는 모든 attribute들이 deep copy되기는 합니다.
import copy

class AA(object):
    def __init__(self, lst, dct):
        self.lst = lst
        self.dct = dct
    
a1 = AA(lst1, dct1)
## 그냥 아래처럼 copy.deepcopy를 사용하면 다 해결되긴 합니다. 
## shallow copy: copy.copy()
a2 = copy.deepcopy(a1)
a3 = copy.deepcopy(a2)
print(id(a1), id(a2), id(a3))
print(id(a1.lst), id(a2.lst), id(a3.lst))
4470978320 4470977312 4470977928
4470748424 4470067336 4470972296

create copy method with copy module

  • copy module의 deepcopy를 사용해서 해당 클래스의 상위 클래스에 copy를 새롭게 정의해줍시다. 상속받는 형태로 상위 클래스에 정의해줘서 다른 모든 클래스들이 다 copy를 사용할 수 있도록 해보죠.
import copy
import numpy as np 

class obj(object):
    ## 이렇게 basic class를 만들고 모두 상속받는 식으로 처리해도 좋습니다. 
    def copy1(self):
        ## __class__(): 마치 cls를 넘겨받는 것처럼 해당 클래스를 생성해줄 수 있습니다. 
        ## self__dict: 내부의 attribute과 각 값ㅇ 접근할 수 있습니다. 
        ## **dict: dictionary 앞에 **를 붙이면, key=value의 형태로 함수에 바로 넘겨줄 수 있습니다. 
        ## 하지만 이 경우에도 해당 class의 내부 attribute에 대해서는 deep copy가 안됩니다. 
        return self.__class__(**self.__dict__)

    def copy2(self):
        ## 이렇게 해당 딕셔너리의 각 v에 deepcopy를 해서 복제해줘도 됩니다. 
        return self.__class__(**{k: copy.deepcopy(v) for k, v in self.__dict__.items()})
        
    def copy3(self):
        ## 근데..그냥 copy module을 사용합시다. 
        return copy.deepcopy(self)
    
class AA(obj):
    def __init__(self, lst):
        self.lst = lst
lst = [1,2 , 3, 4, 5]
a1 = AA(lst)
a2 = a1.copy1()
print(id(a1), id(a2))
print(id(a1.lst), id(a2.lst))
print("="*21)
a2 = a1.copy2()
print(id(a1), id(a2))
print(id(a1.lst), id(a2.lst))
print("="*21)
a2 = a1.copy3()
print(id(a1), id(a2))
print(id(a1.lst), id(a2.lst))
print("="*21)

wrap-up

  • 막상, 다 만들고 나니 굳이 이렇게 할 필요가 있나, 라는 생각이 들기는 하네요.
  • 뭐, 그래도 다음과 같은 대략의 배움이 있는 것 같습니다.
    • shallow copy와 deep copy를 늘 명확하게 인지하자.
    • self.__dir__를 사용하면, 내부의 모든 attribute에 접근할 수 있음.
    • self.__cls__는 마치 cls를 넘겨받는 classmethod에서처럼 class의 변수들에 접근할 수 있다. 또한 self.__cls__()는 해당 클래스의 __init__메소드를 실행시키는 것과 동일함.

reference

댓글남기기