[CPython] Python Object Model (PyObject)
[!info] Python 3.12 이하 CPython 기준으로 작성되었습니다. (no-GIL 이전 버전)
Overview
파이썬은 Object-oriented language(객체 지향 언어)이다. 파이썬의 철학을 한 문장으로 표현하면, ‘Everthing is an object’ 라고 할 수 있다. 그래서, object는 무엇인가?
Objects are Python’s abstraction for data. All data in a Python program is represented by objects or by relations between objects. (In a sense, and in conformance to Von Neumann’s model of a “stored program computer”, code is also represented by objects.) - Python reference
파이썬에서 object는 data의 추상화이다. 어떤 언어든 data는 다룬다.
그러나, 언어마다
- data의 범위가 다르다
- object의 정의가 다르다
대표격 예시로 C와 비교하면:
첫째, C언어에서 함수는 data가 아니라 code(명령어)이다.
C 코드를 컴파일하여 바이너리 파일을 생성하면, 각 함수의 내용물(코드) 실행 가능한 기계어 명령어(executable instruction)로 변환되어 바이너리 파일 내부에 static한 값으로서 저장된다. 이 바이너리 파일을 실행하면 운영체제는 프로세스의 메모리 공간 중 code(text) segment에 이 값들을 매핑한다.
In computing, a code segment, also known as a text segment or simply as text, is a portion of an object file or the corresponding section of the program’s virtual address space that contains executable instructions. … When a program is stored in an object file, the code segment is a part of this file;
C에서 ‘함수 이름’은 virtual address 내에서 ‘해당 함수의 code block 주소’ 그 자체를 나타내는 symbol이다(주소 값을 저장하는 변수의 일종인 ‘함수 포인터’와는 다르다). 따라서 우리는 ‘함수 이름’을 통해 해당 함수의 가상 주소를 알 수 있다. 그러나, 앞서 보았듯이 함수는 executable instruction으로 컴파일 된 상태에서 메모리에 load되기 때문에, 주소를 알고 있다한들 일반 변수에서 행하는 값 수정 등이 조작은 어려울 것이다.
사실 애초에 code segment는 read-only로 설계된 영역이기 때문에 운영체제 단에서 read 외의 동작은 막고 있다. 만약 이렇게 권한이 없는 영역에 쓰기(write)를 시도한다면, segmentation fault 시그널이 발생하고 프로세스는 종료된다.
[!NOTE] code segment는 read-only이며, write를 시도하면 segmentation fault가 발생한다. code segment를 제외한 나머지 영역(data, DSS, stack, heap)은 read/write 모두 가능하다.
그러나 공식 문서에서 보시다시피, 파이썬에서는 code 또한 data로 취급된다. 즉, ‘함수도 object‘이다.
In a sense, and in conformance to Von Neumann’s model of a “stored program computer”, code is also represented by objects.
둘째, C언어에서 object는 data의 abstraction이 아닌, data의 storage이다.
3.14 object
region of data storage in the execution environment, the contents of which can represent values
C에서 object는 ‘data를 저장하는 물리적인 메모리 공간 그 자체’를 의미한다. (참고: symbol과 다르다.)
int id = 393;
// 'id가 가리키는 4바이트의 저장 공간' = object
// 'id'는 symbol
그에 반해 파이썬에서는 ‘data의 추상화’라고 직접적으로 정의내린다.
Objects are Python’s abstraction for data.
대체 파이썬에서 object는 어떻게 구현이 되어있길래 ‘추상화’라고 하는걸까? python의 표준 구현인 Cpython을 통해 알아보자.
Implementation
PyObject
[!Note] 파이썬 공식 문서 소제목의 type 은 파이썬의 클래스인 type과는 무관하며, 오히려 C의
typedef
을 의미한다. 즉, ‘type PyObject’ 는typedef
로 정의된 C타입 구조체 PyObject가 존재함을 의미.
All object types are extensions of this type. This is a type which contains the information Python needs to treat a pointer to an object as an object. In a normal “release” build, it contains only the object’s reference count and a pointer to the corresponding type object. - PyObject
PyObject
는 C 구조체이며, 모든 object 타입들은 PyObject의 extension이다. inheritance(상속)이 아닌 extension(확장)인 이유는 C에서는 상속 관계가 존재하지 않기 때문이다. (그러나 결국에는 상속 관계를 이용하긴 한다. 자세한 내용은 후술.)
사실, PyObject 구조체는 객체라기 보다는 ‘header(헤더)’ 역할에 가깝다. 실질적인 객체의 데이터를 보관하는 용도가 아닌 그 어떤 object 타입이더라도 필요한 정보를 담아두고 띠지로서 기능하는 역할이다. PyObject의 구현은 다음과 같다.
/* Defined in cpython/Include/object.h */
// PyObject
typedef struct _object {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
// PyObject_HEAD
#define PyObject_HEAD PyObject ob_base;
PyObject_HEAD
는 단지 PyObject ob_base;
로 확장되는 매크로이다. 구현에 따라 PyObject_HEAD
매크로 대신 ob_base
변수를 직접 구조체 멤버로 삼기도 한다.
[!note]
PyObject_HEAD
(혹은PyObject ob_base
)는 반드시 Object의 첫 번째 멤버여야한다. 이것은 C에서 pointer type casting(포인터 타입 캐스팅)을 이용한 일종의 ‘우회적 상속 관계 구현’이다. 메모리 배치상PyObject
가 가장 맨 앞에 오기 때문에PyObject
로 취급해도 무방한 것이다. 즉, up-casting(업-캐스팅)과 down-casting(다운-캐스팅)이 가능하다. C-style inheritance 패턴에 대해선 나중에 기회가 있다면 자세히 다뤄보도록 한다.
object 타입을 새로 정의하는데 있어 PyObject를 다음과 같이 사용할 수 있다. PyObject가 Object 구조체의 첫 번째 멤버이기 때문에 up-casting이 가능하다.
// implementation of 'int' type
typedef struct {
PyObject_HEAD
long ob_ival;
} PyLongObject;
ob_refcnt
파이썬의 Garbage Collector(이하 GC)가 메모리 관리를 위해 참고하는 reference count(참조 카운트) 값이다. 이 값은 strong reference(강한 참조)의 수를 나타낸다.
PyTypeObject (ob_type *)
The C structure of the objects used to describe built-in types. - PyTypeObject
PyTypeObject
는 타입의 meta-data를 담고 있는 C 구조체이다. built-in 타입들에 대해 각각의 PyTypeObject이 정의되어 있고, 만약 새로운 타입을 생성한다면 해당 user-defined 타입에 대해서도 동적으로 PyTypeObject가 생성된다.
ob_type
는 PyTypeObject 타입이 아닌 ‘PyTypeObject 포인터’ 타입이라는 점에 유의한다. 왜 포인터일까? 우리는 한 타입에 대해서 여러 개의 인스턴스를 생성할 수 있다. 그런데 같은 타입이라면 특성이 같기 때문에 타입에 대한 description이 굳이 중복되어 저장될 필요가 없다.
그래서 type에 대한 description을 담고 있는 PyTypeObject
구조체는 ‘각 타입별로’ ‘프로세스당 하나씩’ 존재하고, 각 인스턴스는 포인터 변수로 이를 가리킨다. 이는 각 프로세스의 User space에 존재하는 데이터이며, Kernel space와는 무관하다.
[!note] 이렇게 ‘실체는 하나만 두고, 여기저기서 갖다쓰는’ 설계를 singleton pattern(싱글톤 패턴)이라고 부른다.
PyTypeObject
구조체는 다음과 같은 정보를 갖고 있다.
PyObject와는 달리, 별도의 확장 없이 C 구조체 그대로 사용한다.
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; // name of type
Py_ssize_t tp_basicsize; // size of struct
Py_ssize_t tp_itemsize; // size of sequence item
destructor tp_dealloc; // deallocator
printfunc tp_print; // print func
getattrfunc tp_getattr; // get attribute func
setattrfunc tp_setattr; // set attribute func
/* ... 매우 많은 함수 포인터 (연산자 오버로딩, 메서드, 속성 등) ... */
struct _typeobject *tp_base; // base type (상속 관계)
PyObject *tp_dict; // dictionary
/* ... */
} PyTypeObject;
이 구조체 내부에 들어있는 함수 포인터는 객체 지향 언어에 존재하는 개념인 ‘클래스 메서드’와 같은 역할을 한다고 봐도 무방할 것이다. Python 공식 문서에서는 이를 slot 이라 칭한다. 순수 Python 코드로는 slot에 접근할 수 없고, 내부적으로 slot에 접근하는 내장 함수 혹은 dunder method(특별 메서드)를 사용해야한다.
예를 들어, 속성 값을 얻기 위해 tp_getattr
가 가리키는 함수를 사용하고자 한다면 파이썬 코드로 다음과 같이 적으면 된다.
getattr(obj, 'attr_name') # getattr는 내장 함수
지금까지 내용을 전부 이해했다면, 위 예시의 getattr
의 내장 함수가 내부적으로 ‘obj 내부의 PyTypeObject에 접근해 tp_getattr
slot field를 얻고, 이를 호출’ 할 것임을 짐작할 수 있다.
PyTypeObject
또한 PyObject
의 하위 타입이다. 그래서 첫 번째 필드에 어김없이 PyObject
헤더가 들어있다. (C에서의 상속 관계 구현)
그런데 PyObject_HEAD
가 아닌 PyObject_VAR_HEAD
이다. _VAR
가 무엇일까?
PyVarObject
결론만 빠르게 말하자면, _VAR
는 variable(가변)을 의미한다.
파이썬의 타입 중에는 list, tuple, dictionary와 같이 내부 요소의 개개수 가변적인(variable) 타입들이 있다.
자료구조를 직접 구현해본 경험이 있는 사람은 알겠지만, 그러한 container 류의 데이터 타입은 반드시 요소의 개수를 저장하는 필드가 필요하다.
앞서 살펴보았다시피 ob_base *
포인터가 가리키는 PyObjectType 구조체를 통해 타입의 특성과 관련 함수를 얻을 수 있었다. 그러나 문제는 이는 ‘타입별 특성 데이터’로, 개별 인스턴스의 값은 저장되어있지 않다는 것이다.
따라서 ‘PyObject의 필드를 모두 가지면서, 요소의 개수를 저장하는 필드가 있는’ 새로운 구조체가 필요하다는 결론이 나온다. 그것이 바로 PyVarObject
이다.
This is an extension of
PyObject
that adds theob_size
field. This is only used for objects that have some notion of length. This type does not often appear in the Python/C API.
typedef struct {
PyObject ob_base; // PyObject
Py_ssize_t ob_size; // length
} PyVarObject;
#define PyObject_VAR_HEAD \
PyObject_HEAD \
Py_ssize_t ob_size;
PyVarObject
또한 매크로 PyObject_VAR_HEAD
가 존재하며, PyTypeObject
에 헤더로서 첫 번째 멤버로 선언되어있는 것이 바로 이 매크로이다.
[!note]
PyTypeObject
구조체의 첫 번째 멤버는PyVarObject
이고,PyVarObject
의 첫 번째 멤버는PyObject
이다. 이 설계는PyTypeObject → PyVarObject → PyObject
의 상속 구조를 의도한 것으로, 포인터 타입 casting이 가능하다.
[!note] 사실
PyTypeObject
를 가변 객체로서 다루는 일은 거의 없다. 다만 설계상 일관성을 위함이다.
Example
Int (Fixed-size type)
[!info] Python2에서 Int은 fixed-size type인
PyIntObject
로 구현됐지만, Python3에서는 내부적으로 variable-size type인PyLongObject
를 사용한다.
PyIntObject
: fixed-size type, C의 long 타입 범위 내의 정수만 표현PyLongObject
: variable-sized type, 자릿수(길이)가 필요한 만큼 동적으로 확장
// Structure for an instance of the 'int' type
typedef struct {
PyObject_HEAD
long ob_ival;
} PyIntObject;
// Descriptor (type object) for the 'int' type (the 'int' class itself)
PyTypeObject PyInt_Type = {
PyObject_HEAD_INIT(&PyType_Type)
0, // ob_size (fixed size라 항상 0)
"int", // tp_name
sizeof(PyIntObject), // tp_basicsize
0, // tp_itemsize
(destructor)int_dealloc, // tp_dealloc
(printfunc)int_print, // tp_print
(getattrfunc)0, // tp_getattr
(setattrfunc)0, // tp_setattr
// ...
};
PyIntObject
는 Fixed-size이기 때문에 ob_size
필드가 필요하지 않지만 PyTypeObject
구조체의 형식에 맞추기 위해 값 0을 넣는 것을 확인할 수 있다.
List (Variable-size type)
// Structure for an instance of the 'list' type
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
// Descriptor (type object) for the 'list' type (the 'list' class itself)
PyTypeObject PyList_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"list", // tp_name
sizeof(PyListObject), // tp_basicsize
sizeof(PyObject *), // tp_itemsize
(destructor)list_dealloc, // tp_dealloc
(printfunc)list_print, // tp_print
(getattrfunc)0, // tp_getattr
(setattrfunc)0, // tp_setattr
// ...
};
Fixed-size 타입과의 가장 큰 차이점은 tp_itemsize
필드이다.
References
- https://port70.net/~nsz/c/c99/n1256.html#6.9.1
- https://docs.python.org/3/c-api/structures.html#c.PyObject
- https://docs.python.org/3/c-api/structures.html#c.PyTypeObject
- https://docs.python.org/3/c-api/typeobj.html
- https://github.com/python/cpython/blob/2.7/Include/intobject.h
- https://github.com/python/cpython/blob/main/Include/listobject.h