Table of Contents
파이썬 데이터는 객체다
컴퓨터 메모리를 일련의 긴 선반으로 생각할 수 있습니다. 해 당 메모리 선반 중 각 슬롯은 폭이 1바이트 입니다. 파이썬 프로그램은 운영체제에서 컴퓨터의 일부 메모리에 접근할 수 있습니다. 이 메모리는 프로그램 자체의 코드와 데이터를 위해 사용될 수 있습니다. 파이썬은 값을 직접 처리하는 대신, 메모리에 객체로 래핑합니다.
원시타입과 객체
C나 자바 같은 언어는 기본적으로 원시 타입을 제공합니다. 원시 타입은 메모리에 정확하게 타입 크기만큼의 공간을 할당하고 그 공간을 오로지 값으로 채워넣습니다. 배열이라면 요소들이 연속된 순서로 메모리에 배치될 것입니다.
객체는 단순히 값 뿐만 아니라 여러 가지 정보를 함께 저장하고, 이를 이용해 여러 가지 작업(비트조작, 시프팅 등)을 수행할 수 있게됩니다. 하지만 이로 인해 메모리 점유율이 늘어나게 되고 계산 속도 또한 감소하게 되는 단점이 있습니다.
타입
이름 | 타입 | 가변 |
불리언 | bool | 불변 |
정수 | int | 불변 |
부동소수점 | float | 불변 |
복소수 | complex | 불변 |
문자열 | str | 불변 |
튜플 | tuple | 불변 |
바이트 | bytes | 불변 |
프로즌 셋 | frozenset | 불변 |
리스트 | list | 가변 |
바이트 배열 | bytearray | 가변 |
셋 | set | 가변 |
딕셔너리 | dict | 가변 |
가변성
값을 변경할 수 있는 경우를 가변성이라고 합니다. 그러나 파이썬은 강타입 언어이기 때문에 타입을 변경할 수는 없습니다. 즉 객체가 가변성인 경우 값은 변경 가능하지만, 타입은 변경할 수 없습니다. (타입 변경을 하면 무조건 새로운 메모리에 객체가 새로 생성된다는 얘기입니다)
a = "5"
print(type(a))
print(id(a))
a = int(a)
print(type(a))
print(id(a))
------------------
<class 'str'>
139861785283696
<class 'int'>
139861784516640
참조
a = 5
변수에 값을 할당할 때 알아야 할 중요한 사실은 할당은 a라는 변수에 5라는 값을 담는 것이 아니라 a라는 이름이 Int 객체 5를 참조하는 것 입니다. 이를 변수를 통해 객체를 참조한다라고 합니다. 여기에는 예외가 없으며 심지어 문자와 숫자도 모두 객체입니다. 차이는 객체가 불변이냐 가변이냐의 차이일 뿐입니다.
a라는 이름과 객체의 메모리 주소의 매핑 관계는 네임스페이스에 키-밸류 형태로 저장되는데 이 때의 네임스페이스는 메모리 상에서 코드 영역 또는 데이터 영역에 저장된다고 합니다. (스택이나 힙 영역은 아니라고 함, 참고)
예를 들어, 왼쪽 그림에서 a가 참조하고 있던 값을 변경하면 정수는 불변 객체이기 때문에 새로운 값이 메모리에 생성되고 a는 새로운 값을 참조합니다.
반면 오른쪽 그림과 같이 가변 객체는 말 그대로 값을 변경할 수 있기 때문에 자신이 참조하고 있던 값을 변경해도 새로운 메모리에 값이 생성되는 것이 아니라 데이터 값을 그 자리에서 바꾸게 됩니다.
그럼 만약 불변 객체는 값을 바꿀 때 마다 메모리에 새로운 데이터를 생성하게 되는데 그러면 메모리가 엄청 낭비되지 않을까 라는 생각을 할 수 있습니다. 이를 해결해 주기 위해 파이썬에는 가비지 컬렉터가 있고 이는 더 이상 참조되지 않는 객체를 메모리에서 삭제될 수 있도록 도와줍니다.
복사
복사와 관련해서 진짜 복사(copy) 기능을 하는 것이 있고 흉내만 내는 것도 있습니다. 대표적으로 3가지 케이스가 있는데 하나씩 살펴보도록 하겠습니다.
복사란 기존의 값과 같은 값을 가지는 변수를 하나 더 생성하며 각각의 변수는 독립적이어야 한다
Alias
a = [1, 2, 3]
b = a
대입 연산자(=
)를 이용한 경우를 alias(별칭)라고 합니다. 말 그대로 ‘[1, 2, 3]이라는 리스트 객체가 a라는 이름을 가지고 있었는데 b라는 이름을 하나 더 가지게 되었다.’ 정도로 이해할 수 있습니다. 이렇게 되면 a가 가르키는 [1, 2, 3]이 바뀌게 되면 b도 따라서 바뀌게 됩니다.
a[2] = 100
a -> [1, 2, 100]
b -> [1, 2, 100]
(예시: 가수 ‘비’가 있습니다. ‘비’의 본명은 ‘정지훈’입니다. 만약 ‘비’가 머리를 잘랐다면 ‘정지훈’의 머리도 잘립니다.)
Shallow Copy
리스트의 슬라이싱 기능인 :
를 이용하면 alias보다 더 복사같이 느껴집니다. 이를 얕은 복사(Shallow Copy)라고 합니다. 얕은 복사는 어떤 경우에는 정말 복사의 기능을 합니다.
a = [1, 2, 3]
c = a[:]
a[2] = 100
a -> [1, 2, 100]
c -> [1, 2, 3]
이렇게 봤을 때는 충분히 복사의 기능을 하고 있습니다. 하지만 변경하고자 하는 요소가 가변 객체이면 진짜 복사가 아니었다는 것이 드러나게 됩니다.
a = [1, 2, [3, 4, 5]]
c = a[:]
a[2][2] = 100
a -> [1, 2, [3, 4, 100]]
c -> [1, 2, [3, 4, 100]]
새로운 리스트 객체를 생성하긴 하지만 원래의 리스트 객체와 같은 ob_item(요소들의 포인터목록)을 가지고 생성되기 때문에, 요소가 가변 객체일 경우 따라서 변하게 됩니다.
Deep Copy
파이썬의 내장 모듈인 copy를 사용하면 어떤 상황에서도 복사를 제공합니다. 이를 깊은 복사(Deep Copy)라고 합니다. 깊은 복사는 아예 요소 자체를 새로 생성하기 때문에 ob_item도 다른 값을 가지는, 다시 말해서 완전히 같은 값을 새로운 메모리에 할당한 복사가 일어나게 됩니다.
깊은 복사는 어떠한 상황에서도 복사를 보장하기 때문에 안정된 코드를 제공하지만, 메모리 낭비가 발생할 수 있습니다.
import copy
a = [1, 2, 3]
d = copy.deepcopy(a)