본문 바로가기

Python

파이썬 리스트 깊은 복사(deep copy) vs 얕은 복사(shallow copy)

--------

결론부터 말하면... 리스트 복사시에

- 리스트 내부에 중첩리스트가 있으면, 깊은 복사를 써야 완전히 분리되어 복사가 가능하다. 

- 중첩리스트가 없으면 얕은 복사를 쓰는 것이 훨씬 성능이 빠르다. 

 

끝. 

---------

 

파이썬에서 리스트의 일부분을 복사해보자. 

리스트를 복사할 때는 깊은 복사(deep copy)가 좋다고 들었는데, 한번 깊은 복사를 해보자. 

 

먼저 쳇지피티에게 물어보자. 

쳇지피티에게 깊은 복사에 대해 물어보자.

 

아래와 같은 코드를 보여주었다. 

import copy
import random

# 초기 리스트 A (20개의 0으로 초기화)
A = [0] * 20

# 랜덤 값으로 채워진 리스트 B (길이 10)
B = [random.randint(1, 100) for _ in range(10)]

# 깊은 복사 수행
A[:len(B)] = copy.deepcopy(B)

# 결과 출력
print("B:", B)
print("A:", A)

 

실행결과는 다음과 같다. 

실행결과

 

한번 할당받은 메모리는 계속 재사용하는 것이 성능상 좋다고 들었다. 

정말 맞는지 물어보자. 

기존 메모리를 그대로 사용하는지 물어보자.

 

메모리 주소 확인 코드는 다음과 같다. 

import copy
import random

A = [0] * 20  # 초기 리스트 A
B = [random.randint(1, 100) for _ in range(10)]  # 랜덤 리스트 B

print("복사 전 A의 주소:", id(A))

A[:len(B)] = copy.deepcopy(B)  # 깊은 복사 수행

print("복사 후 A의 주소:", id(A))

# 결과 출력
print("B:", B)
print("A:", A)

메모리 주소 확인 결과

메모리 주소 확인 결과, deepcopy를 사용해도 A의 주소가 변경되지 않는 것을 확인할 수 있다. 

쳇지피티도 해당 방법이 성능적으로 유리하다고 한다.

 

그런데 매번 deepcopy라고 타이핑하려니 살짝 귀찮다. 

좀 더 간단하게 하는 방법이 없는지 한번 더 물어보자. 

총 3가지 방법을 알려주는데, 1번 방법 말고 2번, 3번 방법은 거의 본적이 없는 것 같다. 왠지 억지로 2, 3번을 만들어 낸 것 같기도 하고.. 

암튼 아래와 같이 정리를 해주었다. 

정리하자면 중첩리스트(리스트 내부에 또 리스트가 있는 경우)에는 copy.deepcopy()를 사용해야 하고, 

그 외에는 리스트 슬라이싱 방법을 쓰면 될 것 같다. 

 

갑자기 궁금해졌다. A[:len(B)] = B[:] 대신에, 그냥 A[:len(B)] = B 라고만 쓰면 어떻게 될까? 

 

한번 더 물어보자. 

결론적으로로 말하면 A[:len(B)] = B 는 슬라이싱 방식과 동일하다고 한다. 

 

여기까지만 보면 깊은 복사(deep copy)와 얕은 복사(shallow copy)의 차이를 잘 모르겠다. 

깊은 복사를 하던, 얕은 복사를 하던 결과는 동일한데...

 

직접적으로 한번 더 물어보자. 

얕은 복사는 중첩리스트가 있을 때, 완전히 별개로 복사하지 못한다.

즉, 중첩 리스트가 있는 경우 B=A[:] 방식으로 하면 얕은 복사가 되고, 얕은 복사를 하게 되면 B의 내부 리스트를 변경할 때 그 변경값이 원래 있던 A에도 영향을 준다는 말이다. 

깊은 복사는 중첩리스트까지 완전히 분리해서 복사 가능하다.

반면 깊은 복사는 중첩리스트가 있더라도 완전히 별개로 복사되고, 그러므로 하나의 값을 변경해도 다른 하나가 영향을 받지 않는다. 

 

최종적으로 정리하면 다음과 같다. 

얕은 복사 vs 깊은 복사

 

흠, 어찌보면 당연한 이야기지만 깊은 복사가 더 느릴 것 같다. 

그런데 만약... 중첩리스트가 없다면 깊은 복사나 얕은 복사나 비슷할 수도 있을 것 같다. 

한번 더 쳇지피티에게 확인해보자. 

쳇지피티도 거의 성능차이가 없다고 한다.

그리고 아래와 같은 예제코드를 알려주었다. 

import copy
import time

# 1차원 리스트 생성 (중첩 리스트 없음)
A = [i for i in range(1000)]  # 길이 1000짜리 리스트

# ⏳ 얕은 복사 테스트
start_time = time.time()
for _ in range(10000):
    shallow_copy = A[:]  # 얕은 복사
shallow_time = time.time() - start_time

# ⏳ 깊은 복사 테스트
start_time = time.time()
for _ in range(10000):
    deep_copy = copy.deepcopy(A)  # 깊은 복사
deep_time = time.time() - start_time

print(f"얕은 복사 수행 시간: {shallow_time:.4f} 초")
print(f"깊은 복사 수행 시간: {deep_time:.4f} 초")
print(f"깊은 복사가 얕은 복사보다 {deep_time / shallow_time:.2f}배 느림")

 

흠.. 문제는... 실제 내 PC(M1 macbook pro)에서 돌렸을 때, 생각보다 성능차이가 크게 나타났다. 

위의 코드 처럼 둘 다 1만번 복사를 반복했을 때, 생각보다 큰 차이가 났다. 

못믿겠다. 쳇지피티!!!

쳇지피티.. 역시 완전히 믿기에는 무리가 있다. 

혹시나 내 PC의 문제인가 싶어서 다른 PC(Thinkpad X1)으로 동작시켰을 때도 비슷한 결과가 나왔다. 

 

그러므로 리스트 내부에 중첩리스트가 없다면 가능하면 얕은 복사를 쓰자. 

그게 성능면에서 훨씬 빠르다. 

 

리스트에 중첩리스트가 있고, 완전히 분리된 값으로 복사를 해야 한다면 깊은 복사를 써야 한다. 

단, 이때는 성능 저하가 어쩔수 없이 발생한다. 

 

오늘은 여기까지!