본문 바로가기
Python/Coding Base

13. 리스트(List) 2 - 얕은 복사, 깊은 복사

by Nanki 2022. 1. 30.

우선 이글을 읽기전 참조값에 대해 다시 한번 개념 확인 후 읽기를 바란다.

참조값은 메모리에서 리스트 객체의 주소값을 의미한다. 

 

변수 값이 저장되는 순서 : 값(Value) -> 참조값(Reference) -> 메모리(Memory)

얕은 복사(Shallow copy)

Python의 list 변수의 가장 큰 특징

리스트 개체를 직접 저장하지 않는 다는 점이다. 다른 곳에 저장되고 리스트의 참조값(reference)만 변수에 저장된다. 

 

복사에 대한 일반적인 정의를 먼저 알아보자. Ctrl+C , Ctrl+V를 우리는 통상 복사, 붙여넣기라 한다. A라는 파일을 복사한 후 붙여넣기하면 A와 동일하지만 이름만 다른 파일인 A_1이 생겨난다. 이때, 생김새만 동일하고, A를 수정한다고 해서 A_1이 수정되지 않는다. 즉, A와 A_1은 독립이다.

 

리스트를 복사하면 어떤일이 벌어질까? 아래 예시를 보자.

value = [1,2,3,4,5]
value_copy = value

과연 value_copy에 value값이 복사 붙여넣기가 되었을까?

value값을 수정해보겠다. 우리가 예상하는결과는 value_copy의 결과의 원소는 1,2,3,4,5로 바뀌지 않는 것이다.

value[2] = 0
value_copy
## value_copy 출력 결과 : [1, 2, 0, 4, 5]

하지만, value_copy를 출력해보면 value의 3번째 자리를 0으로 바꾸었을 때, value_copy도 동일하게 바뀌어 있는 것을 확인할 수 있다. 이러한 이유는 무엇일까?

 

list 변수의 특징을 좀 더 디테일하게 적어보겠다. 다른 곳에 저장되고 리스트의 참조값(reference)만 변수인 별칭에 저장된다. 즉, value는 리스트 [1,2,3,4,5]의 참조값을 별칭에 저장한 것 뿐이고, 이러한 참조값을 복사하여 다른 별칭인 value_copy에다가 저장한 것 뿐이다. 따라서, value의 리스트 객체를 바꾼다면 value_copy는 그대로 value의 주소를 가져오기 때문에 수정된 value값을 가져온 것이다.

 

자 그렇다면, 리스트를 복사+붙여넣기를 제대로 하려면 어떻게 해야 될까?

 

깊은 복사(Deep copy)

복사+붙여넣기를 하는 것을, 깊게 복사한다 하여 깊은 복사라 부른다. 방법은 총 3가지가 있다.

 

방법1) deepcopy() 또는 copy()메서드를 이용한다. 이 두 함수는 내장 함수로, 리스트를 받아서 복사본을 생성하여 반환한다. 깊은 복사를 하게되면 다른 주소값을 가지게 되어 원본 리스트에는 영향을 끼치지 않는다.

value = [1,2,3,4,5]
value_deep_copy = value.copy()

value[2] = 0
value_deep_copy

## value_deep_copy 결과 : [1, 2, 3, 4, 5]

 

방법2) [:] 인덱스를 이용한다.

value = [1,2,3,4,5]
value_deep_copy2 = value[:]
value[2] = 0
value_deep_copy2
### value_deep_copy2 결과 : [1, 2, 3, 4, 5]

 

얕은, 깊은 복사를 변수의 값과 함수 매개변수의 관계로 알아보자.

얕은, 깊은 복사는 함수 내에서도 자주 이루어지는 작업이다. 

함수에서 인수(매개변수)를 호출하여 불러오는 과정 :

1) 호출하여 다른 변수로 별도 저장하기(Call-by-Value) : b가 수정된다해서 a의 변화가 일어나지 않음(a ≠ b)

  • 호출 변수(a) -> 참조값(Reference) -> 메모리(Memory) -> 값(value) -> 다른 변수(b)에 저장

2) 호출하여 다른 변수에 참조로 저장하기(Call-by-Reference) : b가 수정되면 a의 변화가 일어남(a = b)

  • 호출 변수(a) -> 참조값(Reference) -> 다른 변수(b)에 저장

1)의 과정은 깊은 복사, 2)의 과정은 얕은 복사에 해당된다.

 

함수 매개변수가 깊은 복사가 되는 경우

  • 호출 변수(a) -> 참조값(Reference) -> 메모리(Memory) -> 값(value) -> 다른 변수(b)에 저장

예시를 통해 함수에서의 깊은 복사 과정을 살펴보겠다.

def func1(x) :
	print("함수내 y값",x,"id=",id(x))
	x=0  # 새로운 객체 생성
	print("함수내 y값",x,"id=",id(x))
    return(x)

위와 같은 함수가 있다고 하자. 매개변수 x를 받고 print를 한 뒤, x에 새로운 값을 대입하고 또 다시 print를 하는 함수이다.

y = 11
print("본래 y 값",y,"id=",id(y))
after_y = func1(y)
print("함수 이후 y값",y,"id=",id(y))

print("after_y",after_y,"id=",id(after_y))

하나하나 뜯어보자.

y=11

y의 변수에 11이라는 값을 대입하자. 대입하는 순간 y의 변수는 별칭, 참조값이 생기며 이 참조값 위치의 메모리에 값이 저장된다. id()함수를 통해 참조값 확인이 가능하다.

print("본래 y 값",y,"id=",id(y))
### 결과 : 본래 y 값 11 id= 140735601615568

y의 참조값은 위와 같음을 알 수 있다.

 

아래의 함수를 실행해 보자. 그러면 x에 y가 대입되며 아래와 같은 변화가 일어난다.

func1(y)
  1. print("함수내 y값",y,"id=",id(y))
    ### 결과값 : 함수내 x값 11 id= 140735601615568
  2. y=0  # 새로운 객체 생성
    print("함수내 y값",y,"id=",id(y))
    ### 결과값 : 함수내 y값 0 id= 140735601615216
    return(y)
    ### return된 after_y에 저장되는 값 : 0 id= 140735601615216

 

print("함수내 y값",y,"id=",id(y))
### 결과값 : 함수 이후 y값 11 id= 140735601615568

 

위 결과값들을 살펴보면, 함수의 매개변수 개체 y가 x값에 전달되었고, 바뀐 y값이 함수 중간 과정에서 0으로 값이 바뀌였으나 여전히 y의 결과 값은 11을 유지하는 것을 볼 수 있다. 이는 정수나 문자열이 변경 불가능한 객체이기 때문이다. 

y=0 과정에서 새로운 객체가 생성되었고 이 객체의 참조값이 after_y변수에 저장되었기에, y와 완전히 구분되었다.

 

함수 매개변수가 얕은 복사가 되는 경우 

  • 호출 변수(a) -> 참조값(Reference) -> 다른 변수(b)에 저장

아래 예시를 바로 결과를 살펴보겠다.

def func2(list) :
    list[0] = 999
    return(list)

values = [1,2,3,4,5]

print(values) ### 결과값 : [1, 2, 3, 4, 5]
func2(values)
print(values) ### 결과값 : [999, 2, 3, 4, 5]

함수의 매개변수인 values 리스트의 참조값만이 list값에 전달되었고, 참조값의 첫번째자리가 999로 값이 바뀌었다. 그 결과, func2 함수가 실행 된 후 결과를 보면 [999,2,3,4,5] 결과를 반환한다는 것을 알 수 있다. 이는 리스트는 참조값으로 전달되었기 때문에 함수 안에서 리스트의 요소들의 변경이 가능하다.