파이썬의 `copy` 모듈
이 글은 파이썬의 copy 모듈에 대해 설명합니다.
얕은 복사와 깊은 복사의 차이에 초점을 맞춰, 객체 복제의 기본 메커니즘부터 사용자 정의 클래스에서의 활용까지 실용적인 예제와 함께 명확히 설명합니다.
YouTube Video
파이썬의 copy 모듈
Python의 copy 모듈은 객체 복제(복사)를 다루는 표준 모듈입니다. 목표는 얕은 복사와 깊은 복사의 차이를 이해하고, 사용자 정의 객체의 복사 동작을 제어할 수 있게 되는 것입니다.
기초: 얕은 복사란 무엇인가?
여기서는 리스트와 딕셔너리 같은 가변 객체에 대해 얕은 복사가 어떻게 동작하는지 설명합니다. 얕은 복사는 최상위 객체만 복제하고 내부의 참조는 공유합니다.
1# Example: shallow copy with lists and dicts
2import copy
3
4# A nested list
5original = [1, [2, 3], 4]
6shallow = copy.copy(original)
7
8# Mutate nested element
9shallow[1].append(99)
10
11print("original:", original) # nested change visible in original
12print("shallow: ", shallow)이 코드에서는 최상위 리스트는 복제되지만, 내부 하위 리스트들은 참조가 공유되므로 shallow[1].append(99)의 변경이 original에도 반영됩니다. 이는 얕은 복사의 전형적인 동작입니다.
깊은 복사란 무엇인가?
깊은 복사는 객체와 그 내부의 모든 참조 대상을 재귀적으로 복제하여 독립적인 객체를 만듭니다. 복잡한 중첩 구조를 안전하게 복제하고 싶을 때 사용합니다.
1# Example: deep copy with nested structures
2import copy
3
4original = [1, [2, 3], {'a': [4, 5]}]
5deep = copy.deepcopy(original)
6
7# Mutate nested element of the deep copy
8deep[1].append(100)
9deep[2]['a'].append(200)
10
11print("original:", original) # original remains unchanged
12print("deep: ", deep)이 예시에서 deep에 대한 변경은 original에 영향을 주지 않습니다. deepcopy는 전체 객체 그래프를 복제하여 독립적인 복제본을 만듭니다.
어떤 것을 사용할지 결정하는 방법
어떤 복사를 사용할지는 객체의 구조와 목적에 따라 결정해야 합니다. 내부의 가변 요소 변경이 원본 객체에 영향을 미치지 않도록 하려면 deepcopy가 적합합니다. 이는 전체 객체를 재귀적으로 복제하여 완전히 독립적인 사본을 만들기 때문입니다.
반대로 최상위 객체만 복제하면 충분하고 속도와 메모리 효율을 중시한다면 copy(얕은 복사)가 더 적합합니다.
- 내부 가변 요소를 수정해도 원본이 바뀌지 않게 하려면 →
deepcopy사용. - 최상위만 복제하면 충분하고 성능(속도/메모리)을 우선한다면 →
copy(얕은 복사) 사용.
내장 및 불변 객체 다루기
불변 객체(예: int, str, 내용이 불변인 tuple)는 보통 복사가 필요하지 않습니다. copy.copy는 적절한 경우 동일한 객체를 반환할 수 있습니다.
1# Example: copying immutables
2import copy
3
4a = 42
5b = copy.copy(a)
6print(a is b) # True for small ints (implementation detail)
7
8s = "hello"
9t = copy.deepcopy(s)
10print(s is t) # True (strings are immutable)불변 객체를 복사해도 효율상 이점이 적기 때문에 동일한 객체가 재사용될 수 있습니다. 이는 애플리케이션 설계에서 거의 신경 쓸 필요가 없는 부분입니다.
사용자 정의 클래스: __copy__와 __deepcopy__ 정의하기
클래스에 대해 기본 복사 동작이 기대와 다르다면, 직접 복사 로직을 제공할 수 있습니다. __copy__와 __deepcopy__를 정의하면 copy.copy와 copy.deepcopy가 이를 사용합니다.
1# Example: customizing copy behavior
2import copy
3
4class Node:
5 def __init__(self, value, children=None):
6 self.value = value
7 self.children = children or []
8
9 def __repr__(self):
10 return f"Node({self.value!r}, children={self.children!r})"
11
12 def __copy__(self):
13 # Shallow copy: create new Node, but reuse children list reference
14 new = self.__class__(self.value, self.children)
15 return new
16
17 def __deepcopy__(self, memo):
18 # Deep copy: copy value and deepcopy each child
19 new_children = [copy.deepcopy(child, memo) for child in self.children]
20 new = self.__class__(copy.deepcopy(self.value, memo), new_children)
21 memo[id(self)] = new
22 return new__copy__와__deepcopy__를 구현하면 클래스별 복사 동작을 유연하게 제어할 수 있습니다. 예를 들어, 일부 속성은 동일한 객체를 참조(공유)하도록 하고, 다른 속성은 완전히 새로운 객체로 복제되도록 처리할 수 있습니다.__deepcopy__에 전달되는memo인자는 재귀적 복사 과정에서 이미 처리된 객체를 기록하고 순환 참조로 인한 무한 루프를 방지하는 딕셔너리입니다.
1# Build tree
2root_node = Node('root', [Node('child1'), Node('child2')])
3
4shallow_copy = copy.copy(root_node) # shallow copy
5deep_copy = copy.deepcopy(root_node) # deep copy
6
7# Modify children
8# affects both root_node and shallow_copy
9shallow_copy.children.append(Node('child_shallow'))
10# affects deep_copy only
11deep_copy.children.append(Node('child_deep'))
12
13# Print results
14print("root_node:", root_node)
15print("shallow_copy:", shallow_copy)
16print("deep_copy:", deep_copy)- 이 코드에서 변수
shallow_copy는 '얕은 복사'로 생성되므로 객체의 최상위 수준만 복제되고, 내부에서 참조하는 객체들(이 경우children리스트)은 원본root_node와 공유됩니다. 그 결과shallow_copy에 새 노드를 추가하면 공유된children리스트가 업데이트되고, 그 변경 사항이root_node의 내용에도 반영됩니다. - 반면 변수
deep_copy는 '깊은 복사'로 생성되므로 내부 구조가 재귀적으로 복제되어root_node와 완전히 독립적인 트리가 생성됩니다. 따라서deep_copy에 새 노드를 추가하더라도root_node에 영향을 주지 않습니다.
순환 참조(재귀적 객체)와 memo의 중요성
복잡한 객체 그래프가 자기 자신을 참조하는 경우(순환 참조), deepcopy는 이미 복제된 객체를 추적하기 위해 memo 딕셔너리를 사용하여 무한 루프를 방지합니다.
1# Example: recursive list and deepcopy memo demonstration
2import copy
3
4a = []
5b = [a]
6a.append(b) # a -> [b], b -> [a] (cycle)
7
8# deepcopy can handle cycles
9deep = copy.deepcopy(a)
10print("deep copy succeeded, length:", len(deep))- 내부적으로
deepcopy는 이미 복제된 객체를 참조하기 위해memo를 사용하므로, 순환이 있어도 안전하게 복사할 수 있습니다. 재귀를 수동으로 수행할 때도 이와 유사한 메커니즘이 필요합니다.
copyreg와 직렬화 방식의 사용자 정의 복사(고급)
라이브러리와 연동하여 특별한 복사 동작을 등록하고 싶다면, 표준 copyreg 모듈을 사용해 객체의 (재)구성 방식을 등록할 수 있습니다. 이는 복잡한 객체나 C 확장 타입에 유용합니다.
1# Example: using copyreg to customize pickling/copy behavior (brief example)
2import copy
3import copyreg
4
5class Wrapper:
6 def __init__(self, data):
7 self.data = data
8
9def reduce_wrapper(obj):
10 # Return callable and args so object can be reconstructed
11 return (Wrapper, (obj.data,))
12
13copyreg.pickle(Wrapper, reduce_wrapper)
14
15w = Wrapper([1, 2, 3])
16w_copy = copy.deepcopy(w)
17print("w_copy.data:", w_copy.data)copyreg를 사용하면pickle과deepcopy모두에 영향을 미치는 재구성 규칙을 제공할 수 있습니다. 이는 고급 API이며, 대부분의 경우__deepcopy__만으로 충분합니다.
실무적인 주의점과 함정
copy 모듈을 올바르게 사용하기 위해 유의해야 할 중요한 사항들이 있습니다. 아래에서는 개발 중 마주칠 수 있는 흔한 함정과 이를 피하는 방법을 설명합니다.
- 성능
deepcopy는 메모리와 CPU를 많이 소모할 수 있으므로, 정말 필요한지 신중히 고려하세요. - 공유하고 싶은 요소 처리
대용량 캐시와 같이 일부 속성을 공유 상태로 유지하려면,
__deepcopy__내부에서 해당 참조를 의도적으로 복사하지 않도록 하세요. - 불변 내부 상태 내부에 불변 데이터를 보유한다면, 복사가 불필요할 수 있습니다.
- 스레드와 외부 자원 소켓이나 파일 핸들처럼 복사할 수 없는 자원은 복사해도 의미가 없거나 오류를 유발하므로, 설계 단계에서 복사 대상에서 제외해야 합니다.
실무 예시: 딕셔너리를 안전하게 갱신하는 패턴
복잡한 설정 객체를 갱신할 때, 이 예시는 원본 설정을 보호하기 위해 deepcopy를 사용합니다.
1# Example: safely update a nested configuration using deepcopy
2import copy
3
4default_config = {
5 "db": {"host": "localhost", "ports": [5432]},
6 "features": {"use_cache": True, "cache_sizes": [128, 256]},
7}
8
9# Create a working copy to modify without touching defaults
10working = copy.deepcopy(default_config)
11working["db"]["ports"].append(5433)
12working["features"]["cache_sizes"][0] = 512
13
14print("default_config:", default_config)
15print("working:", working)deepcopy를 사용하면 기본 설정을 손상시킬 위험 없이 파생 설정을 안전하게 만들 수 있습니다. 이는 구성과 같은 중첩된 가변 구조에서 특히 유용합니다.
모범 사례
copy 모듈을 안전하고 효과적으로 사용하려면 다음의 실용적인 지침을 염두에 두는 것이 중요합니다.
- 변경 발생 여부와 그 범위를 고려해 얕은 복사와 깊은 복사 중에서 선택하세요.
- 예상한 동작을 얻기 위해 사용자 정의 클래스에
__copy__와__deepcopy__를 구현하세요. - 깊은 복사는 비용이 크므로, 가능하면 설계를 통해 복사 필요성을 줄이세요. 불변성 적용이나 명시적인 클론 메서드 등의 기법을 고려하세요.
- 순환 참조를 다룰 때는
deepcopy를 활용하거나, memo와 유사한 메커니즘을 수동으로 제공하세요. - 파일 핸들, 스레드 등과 같은 외부 리소스는 복사되지 않도록 설계하세요.
결론
copy 모듈은 Python에서 객체 복제를 위한 표준 도구입니다. 얕은 복사와 깊은 복사의 차이를 올바르게 이해하고 필요할 때 사용자 정의 복사 동작을 구현하면, 안전하고 예측 가능하게 복제를 수행할 수 있습니다. 설계 단계에서 복사가 정말 필요한지와 무엇을 공유할지를 명확히 하면, 불필요한 버그와 성능 문제를 피할 수 있습니다.
위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.