자료형, 메모리, 그리고 Python [1]
프로그래밍에서 자료형(data type)은 여러 기준으로 분류될 수 있다.
다루는 대상이 숫자(numerics)인지 문자열(string)인지, 숫자 중에서도 정수(integer)인지 실수(float)인지.
정수처럼 단일한 대상(scalar)을 말하는지, 아니면 클래스처럼 복합적인 값을 가진 대상(composite)을 말하는지.
언어에 자체적으로 내장된 자료형(built-in)인지, 아니면 무언가를 import해야만 하는 자료형(user-defined)인지.
값을 직접 가리키는지(value), 아니면 값의 주소를 가리키는지(reference).
이 부분에 대해 알아보면서 헷갈리는 게 한두 가지가 아니었다. 각 언어마다 다르게 이야기하기 때문이다. 예를 들어 Java에서는 자료형을 원시 자료형(primitive type)과 참조 자료형(reference type)으로 구분한다. 상술한 기준에 따르면 원시 자료형은 언어에 자체적으로 내장되었느냐를 가리키는 데 적합한 표현이고, 참조 자료형은 값의 주소를 가리키는 경우에 어울리는 이름일 것이다.
이러한 명명의 근원은 모르겠지만 아무래도 교집합이 크기 때문에 일어난 일이라고 생각한다. 보통 int형이라고 하면 단일한 대상이며, 언어에 자체적으로 내장되어 있을 테고, 많은 경우 값을 직접 가리키므로.
물론 예외가 있다. Python이 그렇다. Python에서는 type()이라는 내장 함수가 있는데, 인자로 받은 대상의 자료형을 출력해준다. 예를 들어 print(type(1))라고 입력하면 콘솔에 <class 'int'="">이라는 문구가 출력된다. 여기서 말하는 class란 객체 지향에서 말하는 클래스일까? PyCharm에서 .py 파일에서 int를 입력한 뒤 ctrl + l-click을 하면 해당 명령어가 정의된 부분을 띄워준다. int의 경우에는 builins.py에서 다음과 같이 시작하는 문구를 볼 수 있다.
Python에서는 모든 자료형이 클래스다. 즉 모든 자료가 곧 객체(object)다.
그뿐만이 아니다. Python에서는 모든 변수가 값을 직접 내포하지 않고 주소를 참조하는 reference type이기도 하다.
a = 1
b = 1
c = 2
print(f'id of a: {id(a)}') # id of a: 2507066534192
print(f'id of b: {id(b)}') # id of b: 2507066534192
print(f'id of c: {id(c)}') # id of c: 2507066534224
id() 함수는 객체의 identity를 반환해주는 내장 함수인데, Python의 구현체인 CPython에서는 해당 객체의 메모리 주소를 반환한다. 위와 같은 코드로 Python을 실행하면, 정확한 수치는 달라지겠지만, a의 메모리 주소와 b의 메모리 주소가 같은 것을 볼 수 있다. 심지어는 id(1)이라고 입력해도 같은 주소가 반환된다.
Python에서는 a = 1이라는 코드를 입력하면 1이라는 객체가 메모리 위에 생성되고, 그 주소를 a 변수에 담아주는 방식으로 진행된다. 이러한 특징이 얕은 복사와 깊은 복사의 문제를 야기하기도 한다.
그렇다면 Python에서는 숫자를 입력할 때마다 새로운 객체를 생성하는 걸까? 간접적으로 확인해볼 수 있는 방법이 있다. sys 모듈의 getrefcount()라는 함수다. 이 함수를 사용하면 해당 객체가 얼마나 참조되고 있는지 확인해볼 수 있다.
import sys
print(sys.getrefcount(1)) # 94
print(sys.getrefcount(10)) # 13
print(sys.getrefcount(2147483647)) # 3
a = 1
print(sys.getrefcount(1)) # 95
기이하게도 숫자가 높은 것을 확인할 수 있다. 여기에는 여러 이유가 있는데, sys 모듈에서 참조하고 있어서, getrefcount() 자체가 참조하고 있기 때문에, 그리고 이 StackOverflow의 게시글에 따르면 CPython의 경우 작은 숫자의 경우에는 미리 캐싱해둠으로써 약간의 최적화를 꾀한다고 한다. 실제로 다른 모듈을 import하면 참조 횟수가 달라진다.
import sys
import collections
print(sys.getrefcount(1)) # 109
print(sys.getrefcount(10)) # 13
print(sys.getrefcount(2147483647)) # 3
a = 1
print(sys.getrefcount(1)) # 110
이렇듯 Python에서는 정수 1조차 하나의 객체로써 다뤄지며 우리가 쓰는 변수는 그 객체의 주소를 참조하고 있을 뿐이다. 그렇다면 그 객체는 어디에 살고 있을까? 그곳은 어떤 곳일까? 다음 포스팅에서는 메모리 영역에 대해 알아볼 예정이다.