본문 바로가기

IT

[python] shallow copy, deep copy, pass by assignment

Why?

예상한 대로 메모리 할당이 되지 않는 것 같아서 테스트 진행

서론

  • swallow copy는 원본 메모리를 참조한다.
  • deep copy는 새로운 메모리에 할당한다.
  • 재할당(초기화)는 위와 다른 얘기다(새로운 메모리).

본문

mutable list

  • list
  • set
  • dict

immutable list

  • tuple
  • bool
  • int
  • flout
  • str
  • frozenset

헷갈렸던 부분

python tutor라는 사이트를 발견했고, 이 사이트에서 메모리 테스트를 하다가, 예상과 다르게 동작해서 다시 찾아보게 되었다.

https://pythontutor.com/visualize.html#mode=edit

 

공식 문서의 이론은 다음과 같다.

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

  • 얕은 복사는 새로운 복합 객체를 만들고,(가능한 범위까지) 원본 객체를 가리키는 참조를 새로운 복합 객체에 삽입합니다.
  • 깊은 복사는 새로운 복합 객체를 만들고,재귀적으로 원본 객체의 사본을 새로 만든 복합 객체에 삽입합니다.

파이썬

들어가기 앞서, immutable 객체의 경우, shallow copy와 deep copy가 동일한 결과를 낳는다.

mutable 객체 안에 mutable 객체가 있는 경우

import copy

test = [[1, 2], 3]
a = copy.copy(test)
b = copy.deepcopy(test)

이상하지 않는가? 분명, shallow copy는 "참조"한다고 했는데, a 객체에 대한 메모리를 새롭게 만들었다.

그리고 내부에 있는 mutable 객체만 참조했다.

 

immutable 객체 안에 mutable 객체가 있는 경우

import copy

test = ([1, 2], [3, 4])
a = copy.copy(test)
b = copy.deepcopy(test)

 

왜 a객체가 b객체 처럼 생성될 것을 기대하였는데, a객체는 test tuple객체 참조 하고 있을까?

 

Train of thought

생각을 해보면, 단순하다. mutable 객체라는 것은 변경될 수 있다는 점이다.

위의 공식 문서의 내용을 다시 한번 복기 해보면,

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

  • 얕은 복사는 새로운 복합 객체를 만들고,(가능한 범위까지) 원본 객체를 가리키는 참조를 새로운 복합 객체에 삽입합니다.
  • 깊은 복사는 새로운 복합 객체를 만들고,재귀적으로 원본 객체의 사본을 새로 만든 복합 객체에 삽입합니다.

자칫 넘길 수 있는데 shallow copy는 "새로운 복합 객체"를 만든다는 것이다.

이로 인해서 mutable 객체 안에 mutable 객체가 있는 경우, shallow copy를 한 a 객체가 다른 메모리에 할당 된 것이다.

 

보통 shallow copy, deep copy의 개념이 있다는 것은, 메모리를 효율적으로 사용하겠다는 뜻이다. 값(value)이 똑같다면, 같은 주소를 사용해서 굳이 같은 메모리를 늘리지 않고 같이 사용하겠다는 뜻이다.

python core developers는 이와 같은 생각을 가지고 있을 것이다.

 

그래서 immutable 객체 안에 mutable 객체를 사용하는 2번째 경우를 보면, tuple은 immutable(불변) 즉, 변하지 않는 다는 것이다. 그래서 shallow copy를 진행한, a 객체에 대해서는 test 객체와 동일한 tuple을 사용한다.

 

그럼 왜 shallow copy와 deep copy가 다르게 동작했을까? 분명히, immutable 객체에 대한 shallow copy와 deep copy는 assignment 처럼 동작한다고 하지 않았는가?

 

답은, 내부에 mutable 객체가 있기 때문이다. immutable 객체에 있지만, mutable 객체는 엄연히 변경 가능한 객체이다. 그런데 내부에 있는 mutable 객체에 변화가 일어나면, 그것은 같은 tuple이라고 할 수 있을까?

([1, 2, 3], [8, 9]) != ([1, 2], [8, 9])

 

위 둘은 다른 법이다.

 

import copy

immutable = ([1, 2], 2)

shallow_copyed_immutable = copy.copy(immutable)
deep_copyed_immutable = copy.deepcopy(immutable)

print(id(immutable))
print(id(shallow_copyed_immutable))
print(id(deep_copyed_immutable))


print("=" * 30)


test1 = ([1, 2], [3, 4])
print(f"test1: {id(test1)}")
test2 = copy.copy(test1)
print(f"test2: {id(test2)}")

for i in test2:
    i.append(5)

print(f"test2: {id(test2)}")

print(test1)
print(test2)

 

pass by assignment

파이썬의 함수는 pass by assignment 라는 방식으로 parameter를 넘긴다.(사실 공식 명칭이 없는 것 같다)

 

중요한 점은, pass by assignment는 parameter로 mutable, immutable 값을 넘길 때,

mutable 객체는 call by reference, immutable 객체는 call by value "처럼" 넘긴 다는 것이다.

(python의 모든 메모리는 "객체"에 의하기 때문에 사실 모두 call by reference나 다름 없다.)

 

아무튼, 함수의 parameter에  list, dict, set과 같은 mutable argument를 넘기면, "참조"가 일어나고, 이는 shallow copy와 다름 없기 때문에, parameter에 대한 변경은, 원본의 변경과 다름없다.

 

from typing import List


def func1(arg: List):
    print("func1")
    print(id(arg))
    arg.append(5)


if __name__ == "__main__":
    mutable = [1, 2]
    print(id(mutable))

    func1(mutable)

    print(mutable)

 

 

반면, immutable 객체는 변경을 할 수 있나? 그랬다면 이름 부터가 immutable이 아닐 것이다.

tuple, bool, int, flout, str, frozenset의 값을 변경 할 방법은 "재할당" 말고는 없다.

from typing import Tuple


def func1(arg: Tuple):
    print("func1")
    print(id(arg))
    arg = (4, 5)

    print("arg 재할당 id")
    print(id(arg))


if __name__ == "__main__":
    immutable = (1, 2)
    print(id(immutable))

    func1(immutable)

    print(immutable)

 

 

위와 같이 동작하기 때문에, immutable 객체는 shallow copy나 deep copy가 상관이 없다.

결론

복잡하게 생각 할 것 없다.

메모리 할당 공간 때문에 헷갈릴 수 있으나, 확실하게 이 결론에 다다른다.

 

  • swallow copy는 원본이든, 복사본이든 값이 변경 되면, 서로에게 영향을 끼친다.
  • deep copy는 원본과 복사본 둘다 서로에게 영향을 끼치지 않는다.