안녕하세요. 이번 시간에는 파이썬의 deepcopy에 대해 알아봅시다. 문서의 어떤 부분을 봐야 하는지 유심히 보도록 하겠습니다. 그리고, 어떻게 이러한 기능을 구현해야 하는지도 간단하게 소개해 보겠습니다.

 


 먼저, copy는 shallow copy를 합니다. 문서에서는, compound object를 만들고, 그 안에 있는 objects들의 참조를 복사한다고 되어 있는데요. 2번째 줄에서 li는 아래와 같이 할당 되어 있어요. 그냥 간략하게만 그려 보겠습니다.

 

 li는 이런 식으로 그림이 그려집니다. 여기서 li2 = cp.copy(li)를 했는데요. 참조만 복사한다고 하였습니다.

 

 

 고로, 이렇게 상황이 그려지게 됩니다. 다음에 li[2][1] = 2를 합니다. 이러면 어떻게 될까요? li[2]나 li2[2]나 같은 list를 가리키게 됩니다. 따라서, li[2][1]의 값을 바꾸면, li2[2][1]의 값도 바뀌는 그림이 나옵니다.

 

 다음에 li[0]의 값을 4로 바꾸었습니다. 이 때에는 어떨까요? int가 immutable 하기 때문에, li2가 가리켰던 1과 li가 가리켰던 1을 바꾸지는 않습니다. 대신에, 객체를 새로 생성하게 됩니다.

 

 

 따라서, 실행 결과는 li를 출력하면 [4, 2, [3, 2], [5, 6]]이 나오고, li2를 출력하면 [1, 2, [3, 2], [5, 6]]이 나올 겁니다.

 

 

 정말 그럴까요? 그렇습니다.

 


 반면에 deepcopy는 recursive하게 탐색하면서 복사를 진행하게 됩니다. 아래 프로그램을 봅시다.

 

 

 li를 deepcopy한 것이 li2입니다. 그리고 li[2][1]에 2를 넣었습니다.

 

 

 li2를 출력해 보니, li[2]가 [3, 2]가 아니라 [3, 4]가 나왔어요. 이는, 리스트에 있는 list 객체를 얕은 복사를 한 게 아니라 deepcopy 했기 때문입니다. 일단 여기까지만 보면, 아 그냥 재귀적으로 복사하겠거니. 하고 생각하실 수도 있습니다만, 이 질답글에 있는 답변 내용을 보면 이해가 가지 않으실 겁니다. 특히 이 질문 답변 글에 있는 코드를 보면 뭔가 혼란스러우실 거라 생각해요.

 

 해당 코드를 조금 변형해 보았습니다.

 

 

 li2[0][0]의 값을 10으로 바꿨습니다.

 

 

 결과는 이상하게 나옵니다. li2[0][0]의 값을 10으로 바꿨는데, 왜 li2[1][0]의 값도 10으로 바뀌었을까요? 이는 문서에서 avoid these problem by 부분을 보시면 대강 유추할 수 있어요. 그래도 뭔가 어렵습니다. 문서 초반에 deepcopy는 재귀적으로 뭔가를 한다고 언급을 했으니, 재귀 함수의 본질부터 파악해 봅시다. 재귀에서 가장 무서운 것은, 종료 조건이 없을 때입니다. 왜요? 계속 타고 들어갈 테니까요. 예를 들어 참조 관계가 아래와 같다고 해 보겠습니다.

 

 

 재귀적으로 계속 탐색하면 어떻게 될까요? 결과는 이미 정해져 있습니다. 펑 하고 터질 겁니다. 왜? 1, 2, 3, 1, 2, 3, ... 순으로 계속 파고 들어갈 것이기 때문입니다. 이런 상황을 방지하려면 어떻게 해야 할까요? 종료 조건이 있어야 합니다. 그런데 이런 탐색에서는 어떻게 설정해야 할까요? 비슷한 탐색 방식을 어디선가 본 거 같지 않으신가요? dfs 탐색을 할 때, visit 처리를 하듯, 방문 처리를 하면 됩니다. 그런데 이걸 어떻게 할까요? visit[객체] = true? 사실 특정 시점에서 서로 다른 객체는 서로 다른 id 값을 가지게 됩니다. 이는 문서에서도 언급하고 있는 사항이기도 하고요.

 

 visit에 대한 이야기가 나왔고 dfs가 나왔고, 심지어 복사 관련 문서에도 언급이 되었습니다. 실제로는 조금 더 복잡한 로직으로 구성되어 있지만, 큰 그림은 문서에서 설명하는 것에서 벗어나지 않습니다. deepcopy 기능을 구현하는 것은 코딩 테스트에서도 나올 수 있고, 면접에서도 나올 수 있는 주제이니, 간단하게 언급해 보겠습니다. a를 깊은 복사한 것을 b라 할게요. b를 어떻게 구축할까요? 이는 graph를 복제하는 문제로 볼 수 있어요. 그런데, 그것을 그래프 탐색 알고리즘의 일종인 dfs로 한 것 뿐입니다. 그러니, 그 관점에서 봅시다.

 

 

 아직 객체 1은 방문하지 않았으니, 새로운 1' 객체를 생성 후에, 해시 맵이던 트리 맵에 memo[객체 1] = 객체 1' 이라는 정보를 넣습니다. 이 정보를 넣음으로서, 객체 1을 방문했고, 객체 1에 대응되는 복제 객체는 1'이라는 정보 또한 같이 들어가게 됩니다.

 

 

 다음에 dfs 탐색을 하게 되면, 객체 1이 객체 2와 연결되었다는 정보가 들어오는데요. 2는 방문하지 않았어요. 따라서, 객체 2에 대응되는 것은 객체 2' 이다라는 정보를 추가해요. 자. 그러면 새롭게 복제되는 것에는 객체 1'이 객체 2'과 연결되었다는 걸 넣음 되겠죠? 방향은 1'에서 2'로 가는 것입니다.

 

 

 다음에 3번에 대해서는 어떻게 하면 될까요? 3은 방문하지 않았으니, 3이 복제된 것은 3'이라는 정보를 어딘가에 넣으면 됩니다. 그리고 2에서 3으로 가는 방향이 있으니, 2'에서 3'으로 가는 방향을 추가해 주면 됩니다.

 

 

 다음에 3이 1과 연결되는데요. 어라? 1은 이미 방문했습니다. dfs던 bfs던 방문한 친구는 다시 재방문하지 않아요. 맞나요? 따라서, 더 이상 들어가지 않고 연결 관계만 구축해 주면 되는데요. 3은 3'에 대응된다고 했고 1은 1'에 대응된다고 했으니 3'에서 1'으로 가는 방향을 연결시켜주면 됩니다.