300x250

코테 문제를 풀다가, 리스트를 복사할 때 개고생한(?) 경험을 바탕으로 여러 방법들이 어떤 차이가 있는지 알아보고자 한다.

 

 

 

먼저, 참고로 C++에서의 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)의 개념을 살펴보자면,

복사란 기존 객체와 같은 값을 가진 새로운 객체를 만드는 것이다.

객체는 멤버를 갖고 있고, 멤버들의 값일 수도, 참조 형식일 수도 있다.

이때 객체들이 가진 값 형식과 참조 형식의 복제 방식에 따라 얕은 복사와 깊은 복사의 개념을 나눈다.

객체가 가진 멤버들의 값을 새로운 객체로 복사할 때, 객체가 참조타입의 멤버를 갖고 있다면 참조값만 복사되는 경우가 얕은 복사이다.

 

이에 반해 객체가 가진 모든 멤버 (값과 참조형식 모두)를 복사하는 경우 깊은 복사라 한다.

객체가 참조 타입의 멤버를 포함할 경우, 참조값의 복사가 아닌 참조된 객체 자체가 복사되는 것을 말한다.

 

이제 파이썬에서의 복사를 자세히 살펴보자.

 

 

 

 

 

0. 사전 지식 - 'is'와 '=='의 차이점

 

파이썬으로 코딩을 할 때, 조건문에서 변수가 같은지 두개를 큰 구분 없이 사용해왔다.

하지만 이 두가지는 큰 차이점을 갖고 있다.

바로 'is'는 변수의 Object(객체)가 같을 때 True를 리턴하지만, '=='는 변수의 Value(값)이 같을 때 True를 리턴한다는 점이다.

 

간단한 예시를 들어보자면, 

 

a = []
b = []
c = a

 

위 코드에서, a, b, c는 모두 empty list로, 같은 value를 갖고 있다.

하지만 a와 b는 다른 객체(서로 다른 리스트)이고, a와 c는 같은 객체이다.

이에 따라 'is'와 '=='의 연산을 살펴보면

 

print(a is b)
print(a is c)
print(b is c)

print(a == b)
print(a == c)
print(b == c)

 

결과는 아래와 같다.

 

코드 실행 결과

 

 

 

 

 

1. 할당

 

다음과 같이 리스트를 만들고, 다른 변수에 할당한다고 생각해보자.

 

a = [0, 1, 2, 3, 4]
b = a

 

이렇게 하면 리스트가 두개일 것 같지만, 사실 리스트(객체) 한개를 a라는 이름으로 부르냐, b라는 이름으로 부르냐만 다른 것일 뿐이다.

 

리스트의 할당

 

'0. 사전지식'의 예시에서 보았듯이, 할당을 하게 되면 이와 같은 이유로 같은 객체를 가리키므로 'is'연산자로 비교해보면 True가 출력된다.

 

따라서, 리스트 b의 요소를 변경하면 a와 b 모두에 반영된다.

 

b[2] = 3
print(a)
print(b)

 

결과

 

 

 

 

 

 

2. 복사 (copy와 deepcopy)

 

리스트 a와 b를 완전히 두개로 만들기 위해서는 리스트의 요소들을 복사해야 한다.

 

 

 

 

1) 1차원 리스트의 복사

 

1차원 리스트의 경우, 독립된 다른 리스트를 만들기 위해 copy메소드 또는 모든 요소의 복사를 통해 진행한다.

 

a = [0, 1, 2, 3, 4]
b = a.copy()
c = a[:]

 

copy

 

이 경우, 'is' 연산자로 비교해보면 False가 나온다. 즉 각 리스트는 다른 객체인 것이다.

하지만 복사된 요소는 모두 같으므로 당연히 '=='로 비교하면 True가 출력된다.

 

그리고 각 리스트는 다른 객체이므로, 한 리스트의 요소를 변경해도 다른 리스트에 영향을 주지 않는다.

 

b[2] = 10
c[2] = 15

print(a)
print(b)
print(c)

 

결과

 

 

 

 

 

2) 2차원 리스트의 복사 (Shallow Copy & Deep Copy)

 

하지만, 2차원 리스트는 좀 더 복잡하다.

할당의 경우 마찬가지로 a 2차원 리스트를 b에 할당했을 때, b의 요소를 변경하면 두 리스트 모두에 반영된다.

그런데, 리스트 a를 copy()메서드로 b에 복사하였을 때, 1차원 리스트의 복사에서와 달리 b의 요소를 변경하면 리스트 a와 b 모두에 반영된다.

 

a = [[0, 0], [50, 50]]
b = a.copy()

b[0][0] = 100

print(a)
print(b)

 

결과

 

2차원 리스트에서 copy

 

따라서 2차원 이상의 다차원 리스트에서 리스트를 완전히 복사하려면, 'copy' 모듈의 'copy.deepcopy()'메소드를 사용해야 한다.

 

import copy

a = [[0, 0], [50, 50]]
b = copy.deepcopy(a)

b[0][0] = 100

print(a)
print(b)

 

결과

 

2차원 리스트에서 deepcopy

 

그렇다면, copy와 deepcopy는 어떻게 동작하길래 이런 결과를 가져올까?

먼저, immutable 객체와 mutable 객체를 알아볼 필요가 있다.

영어 뜻 그대로, immutable 객체는 수정이 불가능한 객체, 즉 int, float, 튜플 등을 말하는 것이고

muatble 객체는 수정이 가능한 객체, 즉 리스트, 딕셔너리 등을 말하는 것이라고 생각하면 쉽다.

 

 

 

(1) 얕은복사와 immutable 객체

 

얕은 복사를 진행하면 변수를 복사했다고 생각할 수 있으나, 실제로는 연결되어있다는 것이다.

 

먼저 할당 그림을 다시 살펴보면, a와 b는 변수를 복사하였지만 참조한 곳은 동일하기 때문에 같은 변수를 가리키고 있다고 생각하면 된다.

 

할당

 

사실 복사한 것은 참조(메모리 주소)를 복사한 것이고, 실제 객체 전체를 복사한 것이 아니다. 이는 얕은 복사와 같은 개념으로 볼 수 있다.

 

copy 메소드 사용 시 객체의 종류에 따라 결과가 바뀌게 된다.

immutable한 객체들(int, float, 튜플 등)은 얕은 복사를 하던, 깊은 복사를 하던 상관이 없다.

해당 객체들은 값이 변경되면 무조건 참조가 변경되기 때문에 얕은 복사를 하여 값을 변경하더라도, 참조하던 다른 객체의 값도 변경되는 일이 일어나지 않는다.

 

immuatble 객체인 int 타입에 대해 예를 들어보자.

 

num1 = 3
num2 = num1

num1 = 4

 

위 코드 두번째 줄이 실행되면, 3이라는 값을 갖는 메모리 공간을 num1, num2가 동시에 참조한다.

그런데 num1 = 4를 하면 immutable 객체는 값이 변경될 수 없어 새롭게 메모리를 할당하여 4라는 값을 생성하고 그곳을 num1이 참조하게 되는 것이다.

결론적으로 num1과 num2는 다른 곳을 가리키게 된다.

 

따라서 파이썬에서 얕은복사와 깊은복사에 대해 구분이 필요한 것은 list, set, dictionary 등의 mutable 객체들이다.

 

 

 

(2) mutable 객체들의 얕은 복사와 깊은 복사

 

mutable 객체의 얕은 복사를 진행하는 방법은 다음 4가지이다.

 

  • 대입연산자 '='를 이용한 얕은 복사
    • 이는 위에서 '할당'으로 충분히 설명하였으므로, 넘어가도록 한다. 변수명만 다를 뿐, 리스트가 참조하는 메모리 주소가 똑같다.

 

  • 슬라이싱 '[:]'이나 'copy()'메소드, 또는 copy 모듈의 'copy' 함수를 이용한 얕은 복사 (두 방법 모두 같은 결과)
    • 1차원 리스트의 경우에는 슬라이싱을 이용하여 복사했을 때, 다른 객체가 생성된다고 하였으므로 깊은 복사가 아니냐고 물을 수 있다. 하지만 다차원으로 넘어가게 되면 리스트 자체가 참조하는 메모리 주소는 다르지만, 리스트 내에 존재하는 리스트 (내부 리스트)는 동일한 곳을 가리키고 있다.
    • 따지자면, 완전히 깊은 복사도 아니고, 완전히 얕은 복사도 아닌 것이다.
    • 결론적으로, 리스트 자체에는 깊은 복사가 일어나 값 하나를 변경하였을 때 다른 리스트에 영향을 주지 않지만, 리스트 내부의 리스트에는 얕은 복사가 일어나 값 하나를 변경하였을 때 다른 리스트도 바뀌게 된다.

 

import copy

a = [4, 5, 6, [0, 0, 0]]

# a.copy() 또는 copy.copy(a)도 같은 결과를 나타냄
b = a[:]

print('리스트 자체가 가리키는 메모리 주소')
print(id(a))
print(id(b))

print()

print('리스트 안의 리스트가 가리키는 메모리 주소')
print(id(a[3]))
print(id(b[3]))

print()
print()

# 리스트 끝에 값 추가
b.append(22)

print('리스트 끝에 값 추가 - 깊은 복사')
print(a)
print(b)

print()

# 리스트 안의 리스트에 값 추가
b[3].append(100)

print('리스트 안의 리스트에 값 추가 - 얇은 복사')
print(a)
print(b)

 

결과

 

 

 

위의 얕은 복사에 반해 깊은 복사는 copy 모듈의 deepcopy라는 함수로 사용한다.

deepcopy는 중첩된 리스트(또는 튜플)에 들어있는 내부의 모든 리스트(또는 튜플)까지 복사하여 완전히 새로운 객체를 생성해준다.

모든 것을 새롭게 복사하여 독립적인 객체를 만드는 것이다.

 

위 예시와 같은 과정을 deepcopy를 사용하여 진행해보면,

 

import copy

a = [4, 5, 6, [0, 0, 0]]
b = copy.deepcopy(a)

print('리스트 자체가 가리키는 메모리 주소')
print(id(a))
print(id(b))

print()

print('리스트 안의 리스트가 가리키는 메모리 주소')
print(id(a[3]))
print(id(b[3]))

print()
print()

# 리스트 끝에 값 추가
b.append(22)

print('리스트 끝에 값 추가 - 깊은 복사')
print(a)
print(b)

print()

# 리스트 안의 리스트에 값 추가
b[3].append(100)

print('리스트 안의 리스트에 값 추가 - 얇은 복사')
print(a)
print(b)

 

결과

 

deepcopy

 

위와 같이 a 리스트와 b 리스트는 서로 값 변경 시 전혀 지장을 주지 않는 완전히 독립적인 객체임을 알 수 있다.

 

728x90
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기