반응형

[Python, 파이썬] Call by assignment, mutable, immutable, 파이썬 복사(Python Copy)

반응형

| Call by assignment


파이썬은 함수 호출시 인자를 Call by assignment 방식으로 값을 불러옵니다. Call by assignment는 값에 의한 호출(Call by value), 참조에 의한 호출(Call by reference)과는 다르게 동작합니다. 이것을 이해하기 위한 키포인트는 함수 안에서 넘겨받은 값이 객체의 변경여부에 따라서 어떻게 다르게 동작하는 지를 아는 것입니다. 


함수가 호출될 때는 모두 Call by reference로 불러들입니다. 하지만 mutable이냐 immutable이냐에 따라 함수 안에 객체의 값을 조작할 경우 다르게 동작하게 됩니다. mutable객체일 때는 참조에 의한 호출로서 계속 동작하게 되지만, immutable객체 일때는 값에 의한 호출로 변경된 것같이 동작하게 됩니다. 


즉, 함수에 의해서 호출된 인자는 모두 Call by reference로 레퍼런스가 인자로 들어옵니다. 하지만 함수 안에서 레퍼런스가 가르키는 객체를 조작할 경우, 그 레퍼런스가 가르키는 객체의 변경가능여부가 동작 방식을 결정하게 됩니다. 



| mutable, immutable


파이썬의 Call by assignment와 복사 그리고 변수의 할당 및 대입은 mutable, immutable 객체가 어떻게 변경되고 동작되는 지 알아야 완벽히 이해할 수 있습니다.


두 개념은 간단합니다. mutable은 변경가능한 객체, immutable은 변경 불가능한 객체입니다. 


다음은 파이썬에 쓰이는 자료형들과 mutable, immutable 객체인지를 나타내는 표입니다.



파이썬에서 제공하는 자료형말고 파이썬 유저가 직접 만드는 클래스는 기본적으로 mutable합니다.


mutable, immutable 객체를 구분하는 것은 파이썬을 이해하는 데 매우 중요합니다. 이 객체의 변경여부때문에 파이썬에서 객체와 관련된 동작방식이 달라지기 때문입니다.


| 변수에 대한 할당 및 대 변수간 대입


파이썬에서는 변수에 객체를 할당할 때 변수의 주소값인 레퍼런스를 할당하게 됩니다. mutable, immutable 모두 변수에 값이 할당될 때는 그 객체를 참조하는 레퍼런스가 할당됩니다. 반면 값이 변경될 때는 mutable 객체를 할당했던 변수는 원래의 객체의 레퍼런스를 유지하는데 반해 immutable 객체의 레퍼런스를 할당받은 변수는 변경된 객체의 새로운 레퍼런스를 변수가 할당받게 됩니다.

 
list1 = [1,2,3,4]
print(id(list1))
# 15873408
list1.append(5)
print(id(list1))
# 15873408

tuple1 = (1,2,3,4)
print(id(tuple1))
# 16708256
tuple1 = tuple1 + (5,)
print(id(tuple1))
# 16607440

id는 객체의 고유 레퍼런스를 출력하는 함수입니다. 이 id를 통해서 보면 list1이 변경되었을 때 list1 변수는 같은 객체를 참조하는 것을 알 수 있습니다.

반면 tuple1tuple1에 다른 튜플과 같이 값을 추가해도 새로운 튜플 객체가 만들어 지면서 다른 객체를 참조하고 있다는 것을 알 수 있습니다.

아래는 메모리상에서 위 코드가 어떻게 작동되는 지 설명하는 그림입니다.


파이썬에서 변수간 대입이 이루어질 경우 기존 변수가 가지고 있는 레퍼런스가 할당됩니다. 여기서도 mutable일 경우 객체의 변경이 일어나도 할당된 값이 기존의 객체를 똑같이 가르키는 데 반해 immutable일 경우 새로운 값이 생성되면서 그 객체의 레퍼런스를 새로 할당받게 됩니다. 기존 객체의 레퍼런스가 아닌 새로운 레퍼런스값이 할당되어 다른 객체를 가리키게 되죠.

list1 = [1,2,3,4]
list2 = list1
list1.append(5)
print(id(list1), id(list2))
# 25572736 25572736

tuple1 = (1,2,3,4)
tuple2 = tuple1
tuple1 = tuple1 + (5,)
print(id(tuple1), id(tuple2))
# 26149488 25743696
위 코드를 보면 mutable 객체인 list를 가르키는 list1이 있습니다. list2list1에 레퍼런스를 대입하게 됩니다. 이때 list2list1는 같은 객체를 가르키는 레퍼런스를 가지고 있게 됩니다. tuple1immutable 객체인 tuple을 가르키는 레퍼런스를 가지고 있습니다. tuple2는 이 레퍼런스를 대입을 통해 같은 객체를 가르키는 레퍼런스를 보유한 상태가 됩니다. 하지만 tuple1에 새로운 값이 생성되도 ( (5,) 를 더한 새로운 tuple이 생성 ) tuple2가 가르키는 객체는 같습니다.


| 파이썬 복사, 얕은 복사, 깊은 복사(Python Copy, Shallow copy, Deep copy)


프로그래밍에서는 얕은 복사(shallow copy)깊은 복사(deep copy)라는 용어가 있습니다. 얕은 복사는 어떤 객체의 값은 복사되지만 그 객체에 포함된 레퍼런스가 가리키는 값은 복사되지 않고 단지 그 객체를 가리키는 레퍼런스만 복사되는 것을 뜻합니다. 깊은 복사는 객체에 포함된 레퍼런스가 가리키는 값 또한 모두 새롭게 완벽하게 복사되는 것을 뜻합니다.


파이썬에서는 슬라이싱 혹은 copy 메서드로 대입하게 되면 얕은 복사가 일어나게 되며 객체 안에 있는 객체는 그 객체 자체가 복사되는 것이 아닌 객체의 레퍼런스가 복사되게 됩니다. 만약 깊은 복사를 원하면 deep_copy 메서드를 사용해야 합니다.


말로 듣기에는 어렵지만 다음 코드와 코드의 동작과정을 보면 이해하기 쉬울 것입니다.

import copy
list1 = [[1,2,3,4], [5,6,7]]
list2 = list1[:]  #  or copy.copy(list1) 
list1[0].append(5)
print(list1[0], list2[0])
# [1, 2, 3, 4, 5] [1, 2, 3, 4, 5]
print(id(list1[0]), id(list2[0]))
# 14697296 14697296

list3 = [[1,2,3,4], [5,6,7]]
list4 = copy.deepcopy(list3)
list3[0].append(5)
print(list3[0], list4[0])
# [1, 2, 3, 4, 5] [1, 2, 3, 4]
print(id(list3[0]), id(list4[0]))
# 59094520 59094400

list2list1에 대한 얕은 복사가 이루어 졌습니다. 그 결과 list1[0]에 해당되는 [1,2,3,4]가 바뀌면 list2에 대한 객체도 그 영향을 받게되어 [1,2,3,4,5] 가 출력됩니다.


list4list3에 대한 깊은 복사가 이루어 졌습니다. 그 결과 list3[0]에 해당되는 [1,2,3,4]가 바뀌어도 list4에는 아무 영향도 미치지 않습니다. 서로 다른 객체이기 때문입니다.



반응형

이 글을 공유하기

댓글

Designed by JB FACTORY