파이썬에서의 assignment operation (=)은 객체의 복사본을 만들지 않습니다. 메모리 상에 존재하는 하나의 객체에 대해 다른 변수이름을 binding 하는 것 뿐이죠. 따라서 리스트와 같은 변경 가능한 객체에 대해서 b=a 를 수행하고 a의 원소값을 변경하면 b의 값 또한 마찬가지로 변경됩니다.
>>> a = [1,2,3,4]
>>> b = a
a[3] = 100
>>> a
[1, 2, 3, 100]
>>> b
[1, 2, 3, 100]
하지만 문자열, 정수와 같은 불변 객체에 대해서는 적용되지 않습니다. a의 값을 다른 값으로 변경하면 a는 메모리 상의 다른 객체를 참조하고 b는 그대로 원래 객체를 참조합니다.
>>> a = 10
>>> b = a
>>> b
10
>>> a = 'abc'
>>> b
10
파이썬으로 구현하다보면 특정 객체의 복사본이 필요할 때가 있습니다. 파이썬을 다룬지 얼마 안됬을 때 단순히 =으로 할당했을 때 복사했다고 착각했다가 원본이 변경되어 복사본 (실제로 복사본이 아니죠.)이 변경된 경우가 종종 있었습니다. 우리가 원하는 복사란 원본과 복사본이 별도의 객체로 존재하여 원본에 대한 변경이 복사본에 대해서는 적용되지 않는 것으로 파이썬에서는 shallow copy (얕은 복사)와 deep copy (깊은 복사)가 존재합니다.
Shallow copy
파이썬의 buit-in mutable 구조인 list, set, dictionary 등은 쉽게 얕은 복사가 가능합니다. 또한, copy 내장 라이브러리의 copy 함수로도 얕은 복사가 가능합니다.
new_list = list(original_list)
new_dict = dict(original_dict)
new_set = set(original_set)
from copy import copy
new_list = copy(original_list)
new_dict = copy(original_dict)
new_set = copy(original_set)
그렇다면 왜 "shallow (얕은)" 이라는 용어가 붙었을까요? 복사이니만큼 객체의 복사본을 생성하지만 객체 안의 child 객체까지 복사본을 생성하지 않습니다. 즉, 대상 객체의 껍데기만 복사하는 one-level 복사로 shallow 하다는 용어가 붙은 것입니다. 예를 들어 다음과 같은 리스트 안의 리스트가 있는 복합 객체가 있습니다. is 결과가 False 인 것으로 보아 a와 b는 분명 다른 객체입니다.
import copy
>>> a = [1,[1,2,3]]
>>> b = copy.copy(a)
>>> a is b
False
이 상황에서 a의 첫번째 원소를 변경하면 b는 변경되지 않지만, 두번째 원소인 리스트를 변경하면 b까지 변경됩니다. 즉, 리스트 안의 리스트에 대해서는 복사가 일어나지 않고 a, b 모두 같은 리스트 ([1,2,3])을 참조하고 있는 셈이죠. 하지만 a의 첫번째 원소는 1이어서 immutable 하므로 "a[0]=100" 구문이 a의 첫번쨰 원소는 다른 값으로 바꾸지만 b에는 반영되지 않습니다. 정리하면 얕은 복사는 리스트를 복사하여 다른 객체로 만들긴 하지만 그 객체의 하부 객체 (child object) 까지 복사하지 않고 참조합니다. 따라서 리스트는 mutable 객체이므로 변경되는 것이 복사본이 반영되지만 immutable 하부 객체를 변경했을 때는 하부 객체 자체가 다른 값으로 변경되기에 복사본에는 반영되지 않습니다.
>>> a[0] = 100
>>> print(a, b)
[100, [1, 2, 3]] [1, [1, 2, 3]]
>>> a[1].append(4)
>>> print(a,b)
[100, [1, 2, 3, 4]] [1, [1, 2, 3, 4]]
또한, 다음과 같은 예도 마찬가지입니다. 원본 리스트에 새로운 요소를 추가해도 복사본에는 반영되지 않지만 원본 리스트안의 원래 있던 리스트에 대해서는 복사본도 같은 객체를 참조하므로 변경이 반영됩니다.
>>> xs = [[1,2,3],[4,5,6],[7,8,9]]
>>> ys = copy.copy(xs)
>>> xs.append(['test'])
>>> xs, ys
([[1, 2, 3], [4, 5, 6], [7, 8, 9], ['test']], [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> xs[1][0] = 'Y'
xs, ys
([[1, 2, 3], ['Y', 5, 6], [7, 8, 9], ['test']], [[1, 2, 3], ['Y', 5, 6], [7, 8, 9]])
우리가 만든 클래스에 대해서도 마찬가지로 동작합니다. 다음과 같이 클래스 두개를 선언하고 얕은 복사를 수행해 보겠습니다.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Point({self.x!r}, {self.y!r})'
class Rectangle:
def __init__(self, topleft, bottomright):
self.topleft = topleft
self.bottomright = bottomright
def __repr__(self):
return (f'Rectangle({self.topleft!r}, '
f'{self.bottomright!r})')
rect = Rectangle(Point(0, 1), Point(5, 6))
srect = copy.copy(rect)
이 상황에서 rect 원본의 포인트 값을 바꾸면 srect 객체의 포인트 값은 어떻게 될까요? 얕은 복사를 수행했으므로 객체 안의 하부 객체를 복사하지 않으므로 원본 변경이 복사본에도 반영됩니다.
>>> rect.topleft.x = 999
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))
Deep copy
Shallow copy는 우리가 원하는 진정한 복사가 아닙니다. 얕은 복사는 원본과 복사본이 별도로 존재하기는 하나 완벽하게 독립적으로 존재하지 않습니다. 객체 안의 객체까지 recursive하게 복사본을 만들기 위해서는 copy 라이브러리의 deepcopy 를 사용하여 깊은 복사를 수행해야 합니다. 위의 포인트 예제에서 deepcopy를 수행하면 안의 포인트 인스턴스까지 복사되므로 원본 포인트 변경이 복사본에 반영되지 않습니다. 즉, 깊은 복사는 해당 객체와 child object 까지 재귀적으로 복사함으로써 원본으로부터 완벽히 독립된 복사본을 만듭니다.
>>> drect = copy.deepcopy(srect)
>>> drect.topleft.x = 222
>>> drect
Rectangle(Point(222, 1), Point(5, 6))
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))
참조
'Computer > Python' 카테고리의 다른 글
"is" vs "==" (0) | 2021.08.11 |
---|---|
파이썬 실수 내림/올림 (0) | 2021.08.03 |
List Subtraction (0) | 2021.07.26 |
파이썬의 GIL 사용 이유 (2) | 2021.07.24 |
Decorator 에서 함수 디폴트 인자 파악 방법 (0) | 2021.07.20 |