티스토리 뷰

Python

파이썬 (Python) - 깊은 복사 (Deep Copy)

cra4ckerjack 2020. 6. 26. 14:10

파이썬 (Python) - 깊은 복사 (Deep Copy)


 

 

알고리즘을 풀다 보면 원본배열의 보존을 위해 배열을 복사할 필요를 느낄때가 많다. 

 

객체를 무작정 복사해서 사용하면 원본 객체가 핸들링되어 데이터가 변경되어서 큰 문제를 야기할 수 있기 때문에 객체를 복사할 때에는 주의해서 다뤄야 한다.

 

 

이번 포스팅에서는 객체를 복사하는 다양한 방법에 대해 알아보고자 한다.

 

 

객체를 복사하기 전에 먼저 객체의 특징 중 Mutable과 Immutable의 의미에 대해 알 필요가 있다.

 

 

 


 

[Mutable, Immutable 객체]

 

 파이썬에서 변수는 자신에게 대입된 객체를 가리키는 일종의 포인터 같은 존재이다. 때문에 파이썬에서 변수는 자체 저장공간을 할당받지 않으며 객체를 가리키는 개념이다. 

 

무슨 말인가?

 

C언어에서 변수의 선언을 살펴보자.

int a = 1;

위 Integer 자료형 변수 a는 1의 값을 가지며 변수 a와 1은 같은 존재라고 볼 수 있다.

 

 

파이썬에서 변수의 선언을 보면,

a = 1

파이썬에서는 a와 1은 별개의 존재다.

a라는 변수는 Integer 1이라는 객체를 가리키고 있을 뿐 a의 변수에 정수 1의 값이 할당 된 것이 아니다.

 

Mutable과 Immutable의 개념은 여기서 적용된다. 

Mutable과 Immutable은 글자 그대로, '변한다'와 '변하지 않는다' 라는 뜻을 가지고 있다.

 

 

 

개념을 쉽게 이해하기 위해 다음과 같은 예시를 살펴보자.

a = 1
b = a
print(a, b) # 1 1

위 구문에서 a와 b는 동일하게 (정수 1) 이라는 객체를 가리키게 된다.

 

여기서 b의 값을 2로 바꾸면 어떻게 될까?

a = 1
b = a
print(a, b) # 1 1
b = 2
print(a, b) # 1 2

당현한 것처럼 위의 결과가 출력된다. 

 

이는 int의 자료형이 Immutable한 특징을 가졌기 때문에 그렇다. immutable한 객체는 생성이 된 후 값 수정이 불가능하다.

 

 

 

Mutable한 객체에는 대표적으로 list가 있다.

 

파이썬에서 다음과 같이 배열을 선언해보고 출력해보자.

a = [1, 2, 3, 4]
b = a
print(a, b) # [1, 2, 3, 4] [1, 2, 3, 4]

 

여기에, 위의 방식대로 b의 값을 바꾸면 어떨까? 

a = [1, 2, 3, 4]
b = a
print(a, b) # [1, 2, 3, 4] [1, 2, 3, 4]
b[1] = 0 # 배열 b의 두번째 값을 0으로 바꿔준다.
print(a, b) # [1, 0, 3, 4] [1, 0, 3, 4]

위 결과와는 다르게 원본 배열 a의 두번째 값도 바뀌어 버렸다.

이는 리스트가 Mutable한 특징을 가졌기에 값이 바뀔 수 있기 때문에 원본배열의 값이 바뀌어 버린 것이다.

 

자세히 설명하자면, 위에서 a와 b는 같은 주소값을 참조한다. 그런데 list는 Mutable 하기 때문에 b의 값을 바뀌어 버리면 그 주소값에 있는 값을 바꾸어 버리기 때문에 같은 주소값을 참조하던 a도 값이 바뀌게 나오게 되는 것이다.

 

a = 1
b = a

print(id(a), id(b)) # id() 로 객체가 어떤 주소값(레퍼런스)를 확인하는지 알 수 있다.
print(a is b) # True.   is는 아이디 연산자로 객체가 같은 id값을 가지는지 T/F 를 반환한다.

c = [1, 2, 3, 4]
d = c

print(id(c), id(d)) 
print(c is d) # True

 

위 구문에서 a와 b, c와 d는 각각 동일한 레퍼런스(주소값)를 참조하고 있다.

*정수의 경우 -5 ~ 256 까지 기존 메모리에 생성되어있는 객체를 참조하기에 이 외의 값은 변수를 생성 할 때 다른 주소값을 참조하므로 정확한 말은 아니니 참고하세요

 

하지만 아까와 같은 방식으로 b와 d의 값을 바꿔보면..

 

b = 0
d[1] = 0
print(id(a), id(b))
print(a is b) # False

print(id(c), id(d))
print(c is d) # True

Integer형 변수는 Immutable 하므로 값이 바뀔 수 없어 다른 id값을 가지지만,

list형 변수는 Mutable하므로 값만 바뀌고 여전히 같은 주소값을 참조한다. 때문에 d의 값을 바꿔도 c의 값이 따라서 바뀌는 것이다.

 

 

 

참고) Mutable 자료형과 Immutable 자료형

Class 설명 구분
list 순서가 있는 객체 집합 mutable
set 순서가 없는 고유한 객체 집합 mutable
dict key와 value가 맵핑된 객체, 순서 없음 mutable
bool True, False immutable
int 정수 immutable
float 실수 immutable
tuple 순서가 있는 객체 집합 immutable
str 문자열 immutable
frozenset Immutable Set immutable

출처: https://wikidocs.net/16038

 

 

 


 

 

[얕은 복사(Shallow Copy) 와 깊은 복사(Deep Copy)]

 

 위 개념이 이해가 되었으면 이제 어느정도 감이 잡혔을 것이다.

 

얕은 복사는 객체를 새로운 객체로 복사하지만 원본 객체의 주소값을 복사하는 것이고, 깊은 복사는 전체 복사로 참조값의 복사가 아닌 참조된 객체 자체를 복사하는 것을 말한다.

 

위 예제들에서 리스트들은 모두 얕은 복사를 통해 주소값만 복사되어서 복사된 객체에서 값을 바꾸었던 것이 Mutable한 list의 특성때문에 원본 객체의 값도 바뀐 것을 확인하였다.

 

코딩을 하다보면 원본 배열의 보존을 할 필요가 허다하기 때문에 이럴때는 배열을 '깊은 복사' 하여야 한다.

 

단순함이 강점인 파이썬에서는 깊은 복사를 위한 여러가지 방법이 존재한다.

 

 

- copy모듈의 deepcopy() 이용

import copy

a = [1, 2, 3, 4]
b = copy.deepcopy(a)
b[1] = 0
print(a, b) # [1, 2, 3, 4] [1, 0, 3, 4]

 

가장 많이 쓰이는 방법으로 파이썬 내장 모듈 copy를 import 하여 deepcopy 메소드를 사용하는 방법이다.

copy 모듈의 copy() 메소드와의 차이점은 copy() 는 배열의 내부 객체까지 깊은 복사를 해주지 않는 차이점이 있다. (즉, 이중 이상의 배열은 완전한 깊은복사가 이루어지지 않음)

 

 

- 클래스가 가지고있는 copy() 함수 이용

a = [1, 2, 3, 4]
b = a.copy()
b[1] = 0
print(a,b) # [1, 2, 3, 4] [1, 0, 3, 4]

파이썬의 기본적인 클래스들이 가지고있는 copy() 함수를 이용하는 것으로,  list 클래스 역시 copy() 함수를 가지고 있다.

 

 

- list를 생성할 때 매개변수에 원본을 전달, 혹은 생성후 원본 리스트를 확장

a = [1, 2, 3, 4]
b = list(a)
b[1] = 0
print(a,b) # [1, 2, 3, 4] [1, 0, 3, 4]
a = [1, 2, 3, 4]
b = []
b.extend(a)
b[1] = 0
print(a,b) # [1, 2, 3, 4] [1, 0, 3, 4]

 

- 리스트 슬라이싱

a = [1, 2, 3, 4]
b = a[:]
b[1] = 0
print(a,b) # [1, 2, 3, 4] [1, 0, 3, 4]

 

- 배열 요소로의 접근을 통한 복사

a = [1, 2, 3, 4]
b = [i for i in a]
b[1] = 0
print(a,b) # [1, 2, 3, 4] [1, 0, 3, 4]
a = [1, 2, 3, 4]
b = []
for i in range(len(a)):
    b.append(a[i])
b[1] = 0
print(a,b) # [1, 2, 3, 4] [1, 0, 3, 4]
a = [1, 2, 3, 4]
b = []
for item in a:
    b.append(item)
b[1] = 0
print(a,b) # [1, 2, 3, 4] [1, 0, 3, 4]

 

이렇게 다양한 방법을 통해 깊은 복사를 하면 값은 같더라도 기존에 있던 객체와는 완전히 다른 객체를 하나 만들어 낼 수 있다.

 

리스트 슬라이싱이나 제네릭 copy의 copy() 메소드는 리스트가 오브젝트를 포함할 경우 그 오브젝트들은 얕은복사되는 것만 주의해서 사용하면 된다.

 

어떤 방법을 쓰는게 좋을가? 취향에 따라 쓰면 된다.

 

다만 각 방법 별로 처리속도가 다르니 이는 참고하는게 좋다. 처리속도는 리스트 슬라이싱이 가장 빠르고 copy 모듈의 deepcopy() 메소드가 가장 느리다고 한다.

 

 

 

다음은 깊은복사 방법 별 처리속도를 정리한 것이니 참고 바라며 포스팅을 마친다.

 

<이하 출처:https://stackoverflow.com/questions/2612802/how-to-clone-or-copy-a-list>

 

  • 10.59 sec (105.9us/itn) - copy.deepcopy(old_list)
  • 10.16 sec (101.6us/itn) - pure python Copy() method copying classes with deepcopy
  • 1.488 sec (14.88us/itn) - pure python Copy() method not copying classes (only dicts/lists/tuples)
  • 0.325 sec (3.25us/itn) - for item in old_list: new_list.append(item)
  • 0.217 sec (2.17us/itn) - [i for i in old_list] (a list comprehension)
  • 0.186 sec (1.86us/itn) - copy.copy(old_list)
  • 0.075 sec (0.75us/itn) - list(old_list)
  • 0.053 sec (0.53us/itn) - new_list = []; new_list.extend(old_list)
  • 0.039 sec (0.39us/itn) - old_list[:] (list slicing)

 

 

코드로 직접 처리시간을 검증해 보려면 다음 코드를 실행해 보면 된다.

 

from copy import deepcopy

class old_class:
    def __init__(self):
        self.blah = 'blah'

class new_class(object):
    def __init__(self):
        self.blah = 'blah'

dignore = {str: None, int: None, type(None): None}

def Copy(obj, use_deepcopy=True):
    t = type(obj)

    if t in (list, tuple):
        if t == tuple:
            # Convert to a list if a tuple to 
            # allow assigning to when copying
            is_tuple = True
            obj = list(obj)
        else: 
            # Otherwise just do a quick slice copy
            obj = obj[:]
            is_tuple = False

        # Copy each item recursively
        for x in range(len(obj)):
            if type(obj[x]) in dignore:
                continue
            obj[x] = Copy(obj[x], use_deepcopy)

        if is_tuple: 
            # Convert back into a tuple again
            obj = tuple(obj)

    elif t == dict: 
        # Use the fast shallow dict copy() method and copy any 
        # values which aren't immutable (like lists, dicts etc)
        obj = obj.copy()
        for k in obj:
            if type(obj[k]) in dignore:
                continue
            obj[k] = Copy(obj[k], use_deepcopy)

    elif t in dignore: 
        # Numeric or string/unicode? 
        # It's immutable, so ignore it!
        pass 

    elif use_deepcopy: 
        obj = deepcopy(obj)
    return obj

if __name__ == '__main__':
    import copy
    from time import time

    num_times = 100000
    L = [None, 'blah', 1, 543.4532, 
         ['foo'], ('bar',), {'blah': 'blah'},
         old_class(), new_class()]

    t = time()
    for i in range(num_times):
        Copy(L)
    print('Custom Copy:', time()-t)

    t = time()
    for i in range(num_times):
        Copy(L, use_deepcopy=False)
    print('Custom Copy Only Copying Lists/Tuples/Dicts (no classes):', time()-t)

    t = time()
    for i in range(num_times):
        copy.copy(L)
    print('copy.copy:', time()-t)

    t = time()
    for i in range(num_times):
        copy.deepcopy(L)
    print('copy.deepcopy:', time()-t)

    t = time()
    for i in range(num_times):
        L[:]
    print('list slicing [:]:', time()-t)

    t = time()
    for i in range(num_times):
        list(L)
    print('list(L):', time()-t)

    t = time()
    for i in range(num_times):
        [i for i in L]
    print('list expression(L):', time()-t)

    t = time()
    for i in range(num_times):
        a = []
        a.extend(L)
    print('list extend:', time()-t)

    t = time()
    for i in range(num_times):
        a = []
        for y in L:
            a.append(y)
    print('list append:', time()-t)

    t = time()
    for i in range(num_times):
        a = []
        a.extend(i for i in L)
    print('generator expression extend:', time()-t)

 

 

참조 )

https://docs.python.org/ko/3/library/copy.html

https://wikidocs.net/20707

https://wikidocs.net/16038

https://webnautes.tistory.com/1181

https://blueshw.github.io/2016/01/20/shallow-copy-deep-copy

https://stackoverflow.com/questions/2612802/how-to-clone-or-copy-a-list

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함