위키백과의 정의에 따르면 자료형(data type)이란 "컴퓨터 과학과 프로그래밍 언어에서 실수치, 정수, 불린 자료형 따위의 여러 종류를 데이터로 식별하는 분류로서, 더 나아가 해당 자료형에 대한 가능한 값, 해당 자료형에서 수행을 마칠 수 있는 명령들, 데이터의 의미, 해당 자료형의 값을 저장하는 방식을 결정하는 것"이다.
파이썬을 조금이라도 배운 사람이라면 적어도 자료형이 무엇인지는 익숙할 것이다. int, float, list와 같은 예시들이 떠오를 수도 있을 것이다. 하지만 이런 자료형들은 어떻게 만들어진 것이고 무엇을 위해 존재하는 것일까? 그저 유용하게 사용하기만 했지 나는 이 자료형들의 존재 이유에 관해서는 별로 생각해본 적이 없었다. 이 자료형들의 의미에 대해서 알기위해선 과거 컴퓨터 언어들은 어떤 자료형을 가지고 있었는지 알아보면 좋을 것 같다. 예를 들면 여러 컴퓨터 언어 중 가장 할아버지라고 볼 수 있는 C언어다.
구글에서 C언어의 자료형에 대해서 서치해보면 파이썬과는 사뭇 다른 분위기를 풍긴다. 파이썬은 흔히 수치형으로 퉁치고 넘어가는 자료형이 short, int, long, float, double... 등 바이트 수에 따라 굉장히 세분화하여 분류하고 있다. char의 경우 파이썬의 문자열과는 달리 보통 글자 "하나"(character)를 뜻한다. C언어의 경우 파이썬과는 달리 정적 언어이므로 변수를 선언할 때마다 int a같이 특정 자료형을 지정해주어야 하며 위의 자료형들은 변수를 선언할 때 사용하는 "기본 자료형"들이다.
물론 위의 자료형들은 기본 자료형이고 이 기본 자료형들이 응용되어 더 복잡한 자료형을 만들기도 한다. 하지만 내가 느낀 것은 다른 자료형들도 결국은 저 "숫자"로 귀결된다는 것이다. 문자도 결국 아스키 코드 규칙에 따라 변형된 "숫자"이며, 포인터는 자료가 담긴 메모리의 주소를 "숫자"로 표현한다. C언어에서 배열은 메모리의 연속적으로 저장된 자료 중 맨 앞 자료가 담긴 메모리의 "포인터"이므로 결국 숫자다.
나는 이런 개념들이 상당히 복잡하다고 느꼈고 숫자를 극한으로 이용하면 이렇게 까지 되는구나라고 생각했다. 하지만 한편으로는 간결해보이기도 한다. 하드웨어에는 0과 1로 이루어진 비트들이 존재하고, 이를 이용해 숫자를 표현하고, 이 숫자를 아스키 코드로 해석하면 문자가 되고, 메모리의 위치를 표시하면 포인터가 된다. 그리고 이 자료들을 메모리에 인접하게 저장하면 배열이 된다. 파이썬에선 잘 느껴지지 않던 자료형들 간의 관계가 너무 잘 보인다.
파이썬도 결국 런타임이 C언어로 작성된 프로그래밍 언어다. 파이썬은 C언어와는 매우 다르지만 인터프리터가 작동하고 있는 내부에서는 C언어를 이용해 해석되고 있을 것이다. 굳이 그냥도 사용할 수 있는 C언어를 이용해 파이썬이라는 새로운 언어를 만든 것은 분명한 이유가 있을 것이다. 파이썬의 철학에서 이 이유를 추측할 수 있다. 파이썬은 가독성과 단순성, 효율성을 극도로 추구하고 있다. C언어는 한 번 읽고 이해하기가 어려운 것이 사실이며 낮은 가독성은 개발의 효율성을 떨어트리고 커뮤니케이션을 어렵게 한다. 파이썬은 C언어의 이 단점을 해결하기 위해 등장했다고 볼 수 있다.
여기서 파이썬의 자료형을 어떻게 봐야 좋을지에 대한 해답이 나온다. 파이썬의 자료형은 결국엔 C언어의 자료형을 응용해 더 다양한 기능을 수행할 수 있도록 만들어진 것이다. 그렇다면 이 파이썬의 자료형이 C언어의 어떤 번거로움을 해결하기 위해 만들어진 것인지 알게 된다면 그 자료형의 존재 이유가 밝혀지게 되는 것이다. 파이썬의 각 자료형들을 C언어의 관점에서 본다면 어떤 느낌인지 살펴보겠다. 밑의 자료형들은 파이썬의 공식문서 Built-in Types에 등장하는 분류대로 분류한 것이다.
수치형(numerics)
파이썬의 수치형 자료형은 integer, float, complex 등 수를 나타내는 자료형들을 포괄적으로 부른다. C언어는 수치 자료형을 shor, int, long, float, double등 사용되는 메모리의 바이트를 기준으로 세세하게 나눠놓은 반면 왜 파이썬에서는 수치형을 하나로 뭉뚱그려 묶어서 부르는 경우가 많은 것일까? 파이썬 공식문서에 따르면 파이썬의 integer는 기본적으로 무한한 숫자를 표현할 수 있으며 float는 C언어의 double로 표현된다고 한다. 파이썬은 C언어와 달리 동적 언어이기 때문에 메모리를 할당하고 자료를 집어넣는 C언어와 달리 넣는 자료에 따라 메모리가 자동으로 할당되는 성격이 있기 때문에 굳이 바이트에 따라 수치 자료형을 나누지 않는 것으로 보인다. 또한 수치형에서 파이썬과 C언어가 근본적으로 다른 점은, 파이썬에서는 숫자가 웬만하면 숫자의 용도로 사용된다는 것이다. C언어는 숫자가 문자, 포인터 등의 형태로도 존재하는 것과 달리 파이썬에서는 수치형 자료형의 숫자들이 말 그대로 "숫자"그 자체의 목적으로 존재한다. 즉, 파이썬의 수치형 자료형은 C언어와 달리 상당한 유연성을 가졌으면서 용도 면에서는 더 명확한 자료형이라고 할 수 있다.
시퀀스(Sequence)
파이썬의 시퀀스 자료형은 리스트, 튜플, 문자열, 바이트 등을 포함하는 자료형이다.(다만 문자열과 바이트는 리스트, 튜플과는 조금 다른점이 있다) C언어의 배열과 비슷해보이는데 C언어의 배열과 파이썬의 시퀀스 자료형은 굉장히 큰 괴리감이 느껴졌다. 파이썬이 C언어의 바이트별로 다양한 수치 자료형들을 하나로 통합했다면, 시퀀스 자료형은 C언어의 배열을 세분화, 특수화해서 만들었다는 느낌이 강하다.
파이썬의 시퀀스 자료형은 공통되는 한가지 특징이 있고, 그 특징이 C언어의 배열과의 큰 차이점이다. 그것은 C언어가 데이터를 배열로 저장하는 것과 달리 파이썬은 포인터를 배열로 저장한다는 것이다. 포인터라는 것은 앞에서도 언급했다시피 메모리의 위치를 나타내는 것이다. C언어의 배열은 데이터가 일렬로 저장된 메모리의 맨 앞을 가리키는 포인터이다. C언어가 배열을 다룰땐 포인터를 이용해 배열의 맨 앞부분으로 접근하고, 배열의 끝을 알려주는 메모리가 나올때까지 옆으로 읽어 나간다. 때문에 옆으로 계속 읽어 나가기 위해선 배열이 한가지 자료형만으로 이루어져야 해석할 수 있고, 어디까지 읽어야 하는지 정해줘야 하기에 배열의 길이도 미리 선언해줘야 한다. 반면에 파이썬은 데이터 자체가 아닌 데이터가 담긴 포인터를 배열로 저장하기에 "데이터"의 자료형이 같을 이유가 없다. 어차피 배열 안은 똑같은 포인터 자료형이고, 포인터가 가리키는 데이터의 자료형은 마음대로 바꿀 수 있기 때문이다. 또한 파이썬은 포인터의 배열을 저장할 메모리를 일정 값으로 알아서 선언해주므로 시퀀스의 길이를 직접 선언해 줄 필요가 없다.
그런데 여기서 리스트와 튜플의 차이점이 드러난다. 파이썬에는 객체를 mutable 객체와 immutable 객체로 나눌 수 있는데 mutable 객체는 '뮤턴트' 처럼 값을 바꾸는게 가능하고 immutable 객체는 값을 바꾸는 것이 불가능하다. 리스트는 mutable이고 튜플은 immutable은 이유는, 리스트는 포인터의 배열의 크기가 데이터의 크기에 따라 동적으로 변하는 반면, 튜플은 튜플 생성시의 크기로 고정되기 때문이다. 그래서 리스트는 내부의 데이터가 바뀌더라도 바뀐 크기에 따라 메모리를 알아서 할당하지만 튜플은 그렇지 못하기때문에 오류가 생기는 것이다. 튜플에 + 연산으로 데이터를 추가할 수 있기 때문에 의아하게 생각할 수도 있으나 그것은 튜플에 데이터를 더 붙이는 것이 아니라, 튜플의 데이터와 + 연산해준 데이터를 순서대로 포함한 새로운 튜플을 생성하는 것뿐이다.
C언어의 배열은 튜플과 달리 배열 안의 값을 바꾸는 것에 대해서는 제한이 없다. 어차피 배열 안의 모든 자료형이 동일하기 때문에 다른 값으로 바꾼다고 해서 메모리 사용량이 변하지 않으므로 메모리 크기를 추가로 할당할 필요도 없긴하다. 근데 왜 파이썬은 mutable/immutable 객체를 구분하고 있는 것일까? 대부분의 파이썬 학습서에서 이 부분에 대한 설명은 없이 단순히 리스트는 가변객체이고 튜플은 불변객체라는 차이점이 있다는 식으로 넘어가기 때문에 따로 찾아보는 수밖에 없었다. immutable 객체가 필요한 이유는 크게 두가지로, '스레드 안전(thread-safety)'과 '시간과 메모리의 절약'이다.
스레드 안전은 어느 함수나 변수가 여러 스레드에 의해 호출되더라도 실행에 문제가 없는 것을 말한다. mutable 객체의 경우 값이 변할 수 있기 때문에 어떤 스레드에서 값을 변화시키면 다른 스레드에서의 작업이 잘못될 수 있다. 반면 immutable 객체는 그럴 걱정이 없다. 또한 mutable 객체가 값을 변화시키기 위해 실제로 필요한 메모리보다 더 많은 메모리를 할당하고 있는것과 달리, immutable 객체는 필요한 메모리만 할당시킨다. 때문에 mutable 객체보다 메모리 사용량이 적고 속도도 더 빠르다.
문자열 역시 시퀀스 타입에 속한다. 다만 다른 시퀀스와는 달리 문자 자료형으로 원소들의 자료형이 고정되고, immutable 객체이다. C언어도 문자열을 char의 배열로 다루지만 파이썬 문자열의 편의성이 훨씬 크다. 개인적인 느낌을 말하자면 C언어의 문자열은 문자의 탈을 쓴 배열이라는 느낌이지만 파이썬의 문자열은 더 직관적으로 문자열이라고 느껴진다. C언어에서 문자열을 다루는 것은 매우 어려운 반면 파이썬에서는 쉬운 이유다. 또한 파이썬의 문자열은 문자열끼리의 연산도 제공한다.
매핑(mapping)
매핑 자료형에는 딕셔너리가 속한다. 파이썬의 딕셔너리는 key와 value 구조를 가져서 시퀀스 자료형이 인덱스로 데이터에 접근하는 것처럼 key를 이용해 value에 접근할 수 있다. 딕셔너리의 key가 주어지면 해시함수를 통해 key를 인덱스로 매핑하고, 그 인덱스를 이용해 value를 찾는 방식이다. 이런 자료구조를 해시테이블(hash table)이라고도 한다.
파이썬의 딕셔너리가 특이한 점은 배열과 해시테이블의 특징을 모두 가졌다는 것이다. 3.6버전부터 파이썬의 딕셔너리는 일정한 순서를 가지게 되었다. 그로 인해 for loop에서 더 유용하게 사용할 수 있게 되었다. 이는 파이썬 딕셔너리 자료형의 내부구조에 indicis 를 저장한 배열이 있기 때문이다. 다만 추가적인 메모리를 요구하기 때문에 시퀀스 자료형보다 메모리 낭비가 크다는 것은 단점이다. 파이썬의 딕셔너리는 JSON 형식의 파일을 다루는 데도 매우 유용하다. JSON 형식 자체가 딕셔너리와 완벽히 매칭되기 때문이다.
C언어에서는 딕셔너리와 같은 해시테이블을 사용하려면 직접 구현해야 한다. 이렇게 다른 언어에서는 직접 구현해서 사용해야하는 자료구조를 기본 자료형으로 제공하고 있다는 것은 파이썬의 철학을 잘 보여주는 사례라고 할 수 있다.
집합(Set)
집합 자료형에는 set과 frozenset이 있다. 두 자료형의 차이는 set은 mutable이고 frozenset은 immutable이라는 점이다. 집합 자료형은 데이터의 탐색이 O(1)로 빠르고 데이터들의 유일성을 보장한다. 사실 집합 자료형은 C언어 뿐만 아니라 다른 프로그래밍 언어에서도 잘 볼 수 없는 자료형이다. C언어에서 집합을 구현하는 것은 번거로운 일이라고 하는데 이런 자료형을 파이썬에선 built-in으로 사용할 수 있다는 것이 매우 편리하다.
마치며
글을 쓰다보니 다른 자료형에 비해 시퀀스 자료형에서 글이 너무 길어졌다. 사실 내가 이 글을 쓰게 된 계기가 시퀀스 자료형에 있기 때문이다. 파이썬을 배웠다고 하면서도 나는 이제까지 자료형들이 왜 존재하는지에 대해서 궁금해본적이 없는데, 리스트와 튜플에 대해 생각하다가 "리스트도 튜플이 할 수 있는 작업들을 모두 할 수 있는데 튜플이 왜 필요하지?" 라는 생각이 문득 들게 된 것이다. 나는 평소에도 배열을 쓸 일이 있으면 리스트를 사용하지 튜플을 사용한 적은 거의 없었고 딕셔너리의 items나 함수의 리턴값 정도로나 봐왔기 때문이다. 튜플에 대해 생각하다보니 다른 자료형들에 대해서도 생각하게 되었다. 그래서 결국 이번 기회에 한번 제대로 조사해보자는 생각으로 이 글을 작성하게 된 것이다. 좀 더 C언어랑 잘 비교하면서 본질을 꿰뚫 수 있는 글이 되었으면 좋았을텐데 C언어에 대한 사전 지식이 부족해서 아쉽다. 하지만 이번 기회에 자료형에 대해 깊게 이해하게 되면서 좀 더 파이썬과 가까워진 느낌이다. 앞으로도 종종 내가 가볍게 넘겨버려서 놓쳐버린 인사이트들을 다시 깊게 배우면서 되찾아야겠다.
'Python > Study' 카테고리의 다른 글
파이썬 알고리즘 인터뷰 - 다이나믹 프로그래밍 (0) | 2021.12.01 |
---|---|
파이썬 알고리즘 인터뷰 - 분할 정복 (0) | 2021.11.29 |
파이썬 알고리즘 인터뷰 - 그리디 알고리즘 (0) | 2021.11.29 |
파이썬 알고리즘 인터뷰 - 슬라이딩 윈도우 (0) | 2021.11.26 |
파이썬 알고리즘 인터뷰 - 비트 연산 (0) | 2021.11.25 |