파이썬에서 List의 복사에 대한 다양한 케이스를 다루면서, 관련된 파이썬의 메모리 구조에 대해 이해해보자.
파이썬 버전 3.11.7을 기준으로 작성했다.
1. 1차원 List 곱하기
아래의 코드로 list 곱하기를 실행해보자.
l = [10, 10] * 3
print(l)
[10, 10, 10, 10, 10, 10]
[10, 10]이 3번 반복되어서 [10, 10, 10, 10, 10, 10]이 생성되었다.
* 연산자는 [10, 10]의 복사본을 concatenate해서 이어 붙인다.
정확히 말하자면,
- 정수 10은 immutable 정수 객체이다.
- 파이썬은 먼저 [10, 10] 코드를 evaluate한다. 이때 동일한 "10 정수 객체에 대한 reference인 element"가 2개 있는 list [10, 10]이 생성된다.
- list 곱하기 연산 수행시 [10, 10] 안의 reference들이 3번 반복된 새로운 list(즉 [10, 10, 10, 10, 10, 10])가 생성된다.
- [10, 10, 10, 10, 10, 10]의 각 10은 새로운 객체가 아닌 동일한 10 정수 객체에 대한 reference이다.
List l의 모든 10 인스턴스들은 정수 객체 10이 담겨있는 동일한 메모리 위치를 참조한다.
파이썬에서 정수는 immutable 객체이고, 10은 파이썬에 의해 integer interning이 적용된 small integer 객체이다.
- Integer Interning: 파이썬은 -5 ~ 256 사이의 작은 정수는 별도의 메모리 공간을 미리 할당해서 저장해놓는다. 자주 사용하는 정수를 이렇게 따로 보관함으로써 메모리 사용을 최적화시키는 것이다.
for i in l:
print(id(i))
print(id(10))
140733156967496 # id(l[0])
140733156967496 # id(l[1])
140733156967496 # id(l[2])
140733156967496 # id(l[3])
140733156967496 # id(l[4])
140733156967496 # id(l[5])
140733156967496 # id(10)
id() 함수를 사용해 객체의 주소를 파악할 수 있다. List l의 모든 아이템들이 동일한 메모리(10 정수 객체가 저장된 공간)를 참조하고 있음을 알 수 있다.
Integer interning이 적용되지 않는 정수들의 list에 대해서도 list 곱하기시 아이템들의 주소값을 확인해보자.
l = [1000, 10000] * 3
print(l)
[1000, 10000, 1000, 10000, 1000, 10000]
기존 list의 복사본이 concatenate되는 것은 동일하다.
for i in l:
print(id(i))
print(id(1000))
print(id(10000))
2040756466320 # id(l[0])
2040756466480 # id(l[1])
2040756466320 # id(l[2])
2040756466480 # id(l[3])
2040756466320 # id(l[4])
2040756466480 # id(l[5])
2040754126128 # id(1000)
2040754126128 # id(10000)
- 1000인 l[0], l[2], l[4]의 주소값과 id(1000)의 값이 다르고, 10000인 l[1], l[3], l[5]의 주소값과 id(10000)의 값이 다르다.
- 1000과 10000은 Integer interning이 적용되지 않는 큰 값이기 때문에, 각각 별도의 메모리 공간에 생성된 값을 참조하고 있다.
- id(1000)이 실행되는 순간, global scope에서 새로운 1000 객체가 생성된다(integer interning의 대상이 아니므로 새로 생성).
- 1000인 l[0], l[2], l[4]가 같은 주소값을 갖고, 10000인 l[1], l[3], l[5]가 같은 주소값을 갖는다.
- 메모리에 1000과 10000 객체가 먼저 생성되어 존재하고, 이에 대한 reference가 2번 복사되어 concatenate된 것이다.
- 추가적으로 위 출력결과를 보면 id(1000)과 id(10000)이 동일한 주소값을 갖는 것을 볼 수 있다.
- 이것은 list 곱하기 연산과는 상관없는 내용이다.
- 파이썬의 메모리 사용 최적화로 인한 결과이다.
- id(1000)이 실행되기는 하지만 메모리 할당 이후 주소 출력 외에 딱히 참조되지 않으므로 파이썬 garbage collector가 해당 메모리를 회수해간다. 이후 id(10000)가 호출될 때 같은 주소의 메모리가 할당되었고, 마찬가지로 주소 출력 외에 딱히 참조되지 않으므로 파이썬 garbage collector가 또다시 해당 메모리를 회수해간다.
- 즉 integer interning의 대상이 아닌 1000이나 10000의 값은 파이썬에 의해 항시적으로 할당되는 메모리가 존재하지 않으므로, 참조하는 곳이 없으면 곧바로 메모리가 회수되어버리는 것이다.
아래처럼 l[2]의 값을 수정해보자.
l[2] = 99999
print(l)
[1000, 10000, 99999, 10000, 1000, 10000]
for i in l:
print(id(i))
2040756466320 # id(l[0])
2040756466480 # id(l[1])
2040754126128 # id(l[2])
2040756466480 # id(l[3])
2040756466320 # id(l[4])
2040756466480 # id(l[5])
이제 l[2]의 주소값이 l[0] 및 l[4]와 달라진 것을 확인할 수 있다.
l[2] = 99999가 실행될 때, 메모리에 새롭게 99999 정수 객체가 생성되었고, l[2]는 그 객체에 대한 reference를 갖게 되기 때문이다.
2. 다중 list 곱하기
아래 코드로 다중 list 곱하기를 실행해보자.
l = [[10, 10] * 3] * 2
l[0][0] = 9999
print(l)
[[9999, 10, 10, 10, 10, 10], [9999, 10, 10, 10, 10, 10]]
예상하는 것과 다르게, l[0][0] 뿐 아니라 l[1][0] 값도 바뀐 것을 확인할 수 있다.
이유를 찾기하기 위해 순서대로 분석해보자.
- [10, 10] * 3 실행 시, [10, 10, 10, 10, 10, 10] 가 생성된다.
- [[10, 10] * 3] 실행 시, nested list인 [[10, 10, 10, 10, 10, 10]]가 생성된다.
- [[10, 10] * 3] * 2 실행 시, [[10, 10, 10, 10, 10, 10], [10, 10, 10, 10, 10, 10]]가 생성된다.
- 이때 2개의 안쪽 list(즉 l[0]과 l[1])는 메모리의 동일한 객체를 참조한다. 이 부분이 핵심이다.
- l[0][0] = 9999
- 첫 안쪽 list의 첫 element인 l[0][0]을 수정한다.
- 2개의 안쪽 list가 동일한 list 객체를 참조하고 있기 때문에 두번째 안쪽 list의 첫 element도 수정된다.
pythontutor.com 을 활용해서 위 과정을 그려봤다.
1. l = [[10, 10] * 3] *2 실행 후
list 곱하기 연산이 진행된 상태인데, 2개의 안쪽 list는 동일한 list 객체를 참조하고 있음을 알 수 있다.
2. l[0][0] = 9999 실행 후
2개의 안쪽 list가 동일한 list를 참조하고 있으므로, 한쪽에서 해당 list의 element 값을 수정하면 다른 쪽도 영향을 받음을 알 수 있다.
요약
1차원 list이든 다중 list이든 상관없이, list의 각 element는 특정 객체에 대한 reference를 나타낸다(파이썬에서는 변수가 객체를 참조하기 때문이고, list element도 동일하다).
따라서 list에 대한 곱하기 연산이 수행될 때, list의 element인 객체의 값이 복제되는 것이 아니라 reference가 복제되는 것이다.
1. 1차원 list의 곱하기 연산의 경우, list element는 immutable 객체(이 경우 정수)에 대한 reference이며, 곱하기 연산시 해당 immutable 객체에 대한 reference가 복제된다.
[10, 10] * 3
2. 다중 list의 곱하기 연산의 경우, list element는 안쪽 list 객체에 대한 reference이며, 곱하기 연산시 해당 list 객체에 대한 reference가 복제된다.
[[10, 10] * 3] * 2
'Python > 문법' 카테고리의 다른 글
[Python] 클로저(Closure) 함수 (0) | 2024.12.11 |
---|---|
[Python] 객체의 shallow copy와 deep copy (1) | 2024.12.10 |
[Python] Asterisk(*)가 pack과 unpack을 수행하는 방식 (2) | 2024.12.06 |
[Python] Static Method (@staticmethod) (0) | 2024.12.05 |
[Python] Class 변수와 Instance 변수 사용시 조심할 점 (0) | 2024.12.05 |