TIL18. Python : Copy & Deep Copy

32868 단어 pythonpython

📌 이 포스팅에서는 Python의 객체 참조의 이해를 통해 얕은 복사와 깊은 복사에 따른 차이점에 대해 알아보겠습니다.



🌈 Copy & Deep Copy

🔥 객체의 참조와 비교연산자

🔥 얕은복사(Copy) 깊은복사(Deep Copy)란 무엇일까?

🔥 가변형과 불변형의 객체 참조



1. 객체의 참조와 비교연산자

🤔 "==" 과 "is" 차이점

✔️ x에 객체를 할당하고 y에 x를 할당시키면, x와 y는 모두 같은 메모리를 참조합니다.
✔️ 이에 서로 값을 비교하면 True를 반환하고, id값도 서로 동일한 것을 볼 수 있습니다.
✔️ 즉, x,y는 모두 같은 객체를 가르키기 때문에 이 중 하나를 수정하면 모두 수정됩니다.

x = {"name": "Jang", "age": 33, "city": "Seoul"} # 👈 x라는 변수에 객체를 할당합니다.
y = x # 👈 y라는 변수에 x가 가르키는 참조값을 가르킵니다.
print(x == y)  # True
print(x is y)  # True
print(id(x), id(y))  # 140643930504064 140643930504064
print(id(x) == id(y))  # True
x["sex"] = "Male"
print(x, y)  # {'name': 'Jang', 'age': 33, 'city': 'Seoul', 'sex': 'Male'} {'name': 'Jang', 'age': 33, 'city': 'Seoul', 'sex': 'Male'}

✔️ 같은 값을 x와 z에 각 각 객체를 할당하고 비교하면, 값은 같지만 다른 독립적 객체임을 알 수 있습니다.
✔️ 동등연산자(==)는 값을 비교하고, is는 같은 객체인지 객체 주소를 비교하는데, 많은 데이터를 비교할 때, is가 ==보다 속도가 빠릅니다.
✔️ 객체 안에 모든 요소를 비교할 필요없이 같은 객체인지 비교하기 때문입니다.

x = {"name": "Jang", "age": 33, "city": "Seoul", "sex": "Male"}
z = {"name": "Jang", "age": 33, "city": "Seoul", "sex": "Male"}
print(x, z) # {'name': 'Jang', 'age': 33, 'city': 'Seoul', 'sex': 'Male'} {'name': 'Jang', 'age': 33, 'city': 'Seoul', 'sex': 'Male'}
print(x == z)  # True
print(x is z)  # False
print(id(x), id(z))  # 140260488281024 140260488281280


2. 얕은복사(Copy) 깊은복사(Deep Copy)란 무엇일까?

🤔 변수의 할당과 복사는 어떤 차이가 있을까?

✔️ copy는 얕은 복사라 불리고, 값을 할당하는 것과는 다소 차이가 있습니다.
✔️ 값을 새로운 객체를 생성해주는 list()에 담으면, 이를 얕은 복사(copy)이기 때문에 값은 같지만 다른 객체입니다.
✔️ 이에 값을 비교하면 True가 나오지만, is로 객체를 비교하면 False를 반환합니다.

data1 = [10, [100, 105], (5, 10, 15)]
data2 = data1         # 👈 할당
data3 = list(data1)   # 👈 copy
print(data1 == data2) # True
print(data1 is data2) # True
print(data1 == data3) # True
print(data1 is data3) # False

✔️ 객체에 값을 추가하면, data1, data2 객체에서만 값이 추가되고, 객체 내 원소 중 리스트 내에 값을 remove했더니 data1, data2, data3 모두 값이 제거됩니다.
✔️ 이 차이는 객체 data1, data2는 같은 객체이고 data3는 다른 객체지만, 객체 내 원소들이 동일한 id값을 가지기 때문입니다.
✔️ 즉, 얕은 복사를하면 복사한 객체 자체는 다른 객체로 나오지만 객체 내 요소들은 할당했을 때 처럼 같은 id 값을 갖고 있습니다.

data1 = [10, [100, 105], (5, 10, 15)]
data2 = data1         # 👈 할당
data3 = list(data1)   # 👈 copy
print(data1 == data2) # True
print(data1 is data2) # True
print(data1 == data3) # True
print(data1 is data3) # False
# 수정
data1.append(1000)
data1[1].remove(105)
print(data1)  # [10, [100], (5, 10, 15), 1000]
print(data2)  # [10, [100], (5, 10, 15), 1000]
print(data3)  # [10, [100], (5, 10, 15)]

✔️ 이번에는 객체 내 리스트와 튜플에 접근하여 값을 추가해보겠습니다. 수정하기 전 객체 내 요소의 id값을 확인해보면 모두 동일한 것을 확인할 수 있습니다.
✔️ 객체 내 리스트의 수정을 발생시키면, 모두 같은 id값을 갖고 있기 때문에 수정되는 것을 볼 수 있습니다. 단, 튜플에서는 다른 양상을 보입니다.
✔️ 튜플은 불변이기 때문에 수정이 발생되면 새로운 객체를 생성합니다. 이에 data1[2]과 data2[2]의 id값은 함께 변했지만, data3[2]는 id값이 그대로 입니다.
✔️ 새로운 객체를 생성한다는 것은 메모리를 소비하고, 헷갈리기 때문에 리스트 안에 튜플을 넣는 것은 추천하지 않습니다.

print(id(data1[1]), id(data2[1]), id(data3[1])) # 140677041846656 140677041846656 140677041846656
print(id(data1[2]), id(data2[2]), id(data3[2])) # 140677041885632 140677041885632 140677041885632
data1[1] += [110, 120]
data1[2] += (110, 120)
print(data1)  # [10, [100, 110, 120], (5, 10, 15, 110, 120), 1000]
print(data2)  # [10, [100, 110, 120], (5, 10, 15, 110, 120), 1000]
print(data3)  # [10, [100, 110, 120], (5, 10, 15)]
print(id(data1[1]), id(data2[1]), id(data3[1])) # 140677041846656 140677041846656 140677041846656
print(id(data1[2]), id(data2[2]), id(data3[2])) # 140677003933712 140677003933712 140677041885632

🤔 얕은 복사(Copy)와 깊은 복사(Deep Copy) 차이

✔️ 깊은 복사는 말 그대로 뿌리까지 복사합니다. 즉, 객체 내부의 각 요소까지 독립된 id값을 가집니다.
✔️ 얕은 복사를 했을 때 어떠한 차이를 보이는지 아래 장바구니 코드에서 확인해보겠습니다.
✔️ basket2는 basket1을 얕은 복사하였고, basket3는 basket1을 깊은 복사하였습니다.
✔️ 이 결과, 3개의 인스턴스는 모두 다른 다른 객체지만, 객체 내부의 products를 살펴보면, "basket1._products"과 "basket2._products"이 같은 id를 갖고있습니다.

class Basket:
    def __init__(self, products=None):
        if products is None:
            self._products = []  # 👈 상품이 없을 경우,
        else:
            self._products = list(products)  # 👈 상품이 이미 있는데 추가될 경우,
    # 상품 추가
    def put_prod(self, prod_name):
        self._products.append(prod_name)
    # 상품 제거
    def del_prod(self, prod_name):
        self._products.remove(prod_name)
basket1 = Basket(["Apple", "Bag", "TV", "Snack", "Water"])
basket2 = copy.copy(basket1)  # 👈 얕은 복사
basket3 = copy.deepcopy(basket1)  # 👈 깊은 복사
print(id(basket1), id(basket2), id(basket3))  # 140659652628384 140659652627856 140659652627184
print(id(basket1._products), id(basket2._products), id(basket3._products))  # 140687808135040 140687808135040 140687808003776

✔️ 이는 같은 메모리값을 참조하고 있기 때문에 basket1에서 수정이 발생하면, basket2의 상품도 수정되는 일이 발생합니다.
✔️ 이에 복사한 결과가 내부적으로도 독립적으로 처리되길 원한다면 깊은 복사를 해야합니다.

basket1.put_prod("MacBook")
basket2.del_prod("Snack")
print(basket1._products) # ['Apple', 'Bag', 'TV', 'Water', 'MacBook']
print(basket2._products) # ['Apple', 'Bag', 'TV', 'Water', 'MacBook']
print(basket3._products) # ['Apple', 'Bag', 'TV', 'Snack', 'Water']


3. 가변형과 불변형의 객체 참조

🤔 함수의 parameter로 전달될 때의 객체 참조

✔️ 아래 plus 함수는 x,y를 parameter로 전달 받아 그 값을 더한 후 x에 할당하여 반환합니다.
✔️ parameter로 가변형과 불변형이 전달될 때, 어떤 차이가 발생하는지 알아보겠습니다.

def plus(x, y):
    x += y
    return x

✔️ 불변형 str과 tuple은 원본데이터의 수정이 발생하지 않습니다. 즉, parameter가 전달되다는 것은 참조값을 전달시키는데 불변형이기 때문에 수정이 발생되면 새로운 객체가 생성됩니다. 이에 함수 밖에서 c의 id값과 함수 내에서 반환되는 c의 id는 다른 값입니다.

x = 4
y = 8
print(plus(x, y), x, y)  # 12 4 8
c = (10, 100)
d = (5, 10)
print(plus(c, d), c, d)  # (10, 100, 5, 10) (10, 100) (5, 10)

✔️ 가변형일 때도 전달되는 parameter는 참조값이 전달됩니다. 다만, 가변형일 때는 함수 외부의 a와 반환되는 a가 같은 메모리 주소를 가르키기 때문에 원본 데이터의 수정도 발생됩니다.
✔️ 이에 함수 밖에서 a의 id값과 함수 내에서 반환되는 b의 id는 같은 값입니다.

def plus(x, y):
    x += y
    return x
# 가변형일 때는 데이터가 변경됩니다. 참조값을 parameter로 전달했기 때문입니다.
# 원본 데이터가 변경되어도 될 때는 리스트
a = [10, 100]
b = [5, 10]
print(plus(a, b), a, b)  # [10, 100, 5, 10] [10, 100, 5, 10] [5, 10]

✔️ 이에 데이터를 parameter로 전달할 때, 원본의 수정이 발생되도 문제 없다면 가변형인 list를 사용하고, 원본의 수정이 발생되면 안될 때는 tuple을 사용해야합니다.

🤔 불변형은 객체 복사가 될까?

✔️ 불변형 데이터 타입들(str, bytes, frozenset, tuple...)은 데이터가 복사되지 않고 무조건 원본을 참조하여 반환됩니다.
✔️ 어차피 불변형 타입은 값의 수정이 발생하면 새로운 객체를 반환하기 때문에 효율성을 위해 할당을하던 복사를하던 원본 데이터를 참조합니다.

tt1 = (1, 2, 3, 4, 5)
tt2 = tuple(tt1)  # 👈 얕은 복사
tt3 = tt1[:]
print(id(tt1), id(tt2), id(tt3)) # 140317351802896 140317351802896 140317351802896
print(tt1 is tt2) # True
print(tt1 is tt3) # True

✔️ 심지어 str, tuple 등 불변형은 효율성을 위해 같은 값이면 아예 별개로 생성해도 같은 곳을 참조합니다.

tt4 = (10, 20, 30, 40, 50)
tt5 = (10, 20, 30, 40, 50)
ss1 = "Apple"
ss2 = "Apple"
print(tt4 is tt5, tt4 == tt5) # True True
print(id(tt4), id(tt5)) # 140323132363296 140323132363296
print(ss1 is ss2, ss1 == ss2) # True True
print(id(ss1), id(ss2)) # 140323162495920 140323162495920

좋은 웹페이지 즐겨찾기