본 글은 전문가를 위한 파이썬 2판을 읽고 정리한 글입니다.
PyObject와 같은 기본 개념과 C 코드를 대략 읽을 수 있는 독자를 대상으로 합니다.
1장은 파이썬의 일관성과 관련된 데이터 모델에 대해 이야기합니다.
데이터 모델이 제공하는 API를 잘 사용하기 위해 Magic Method 혹은 Dunder Method의 강력함을 소개합니다. 이를 구현하면 사용자 정의 객체도 파이썬 내장 객체처럼 작동하므로 파이썬 다운 표현력 있는 코딩 스타일을 사용할 수 있음을 강조합니다.
예를 들어 봅시다.
Java의 경우 아래처럼 길이를 호출하는 여러 함수가 있습니다.
// Java가 떠오른다...
int[] array = {1, 2, 3};
int arrayLength = array.length;
String str = "hello";
int strLength = str.length();
ArrayList<Integer> list = new ArrayList<>();
int listSize = list.size();
하지만 파이썬의 경우 아래와 같은 자료형이 모두 len 으로 길이를 알아낼 수 있죠. 그뿐만 아니라 사용자 정의 클래스도 len 을 구현했다면 len 메소드를 사용할 수 있습니다.
my_list = [1, 2, 3]
my_string = "hello"
my_tuple = (1, 2, 3)
my_dict = {"a": 1, "b": 2}
my_set = {1, 2, 3}
# len(my_list), len(my_string), len...
len()은 abs()와 마찬가지로 파이썬 데이터 모델의 특별 대우를 받으므로 메서드라고 부르지 않는다는 점이 재미있었습니다. len()은 CPython의 내장 객체에 대해서는 메서드를 호출하지 않고, 길이는 단지 C구조체의 필드를 읽어옵니다. builtin_len
의 코드입니다.
// Python/bltinmodule.c
static PyObject *
builtin_len(PyObject *module, PyObject *obj)
{
Py_ssize_t res;
res = PyObject_Size(obj);
if (res < 0) {
assert(PyErr_Occurred());
return NULL;
}
return PyLong_FromSsize_t(res);
}
---
// Objects/abstract.c
Py_ssize_t
PyObject_Size(PyObject *o)
{
if (o == NULL) {
null_error();
return -1;
}
PySequenceMethods *m = Py_TYPE(o)->tp_as_sequence;
if (m && m->sq_length) {
Py_ssize_t len = m->sq_length(o); // sq_length 메서드 호출
assert(_Py_CheckSlotResult(o, "__len__", len >= 0));
return len;
}
return PyMapping_Size(o); #map 같은 객체들
}
실제로 파이썬 내부 오브젝트의 sq_length는 어떤 식으로 구현되어 있나 궁금해서 찾아보았습니다. 아래는 list의 예시로 C 구조체의 ob_size 필드를 직접 읽고 있음을 확인할 수 있습니다.
// Objects/listobject.c
static PySequenceMethods list_as_sequence = {
list_length, /* sq_length */
list_concat, /* sq_concat */
...
};
static inline Py_ssize_t PyList_GET_SIZE(PyObject *op) {
PyListObject *list = _PyList_CAST(op);
#ifdef Py_GIL_DISABLED
return _Py_atomic_load_ssize_relaxed(&(_PyVarObject_CAST(list)->ob_size));
#else
return Py_SIZE(list);
#endif
}
---
//Include/cpython/listobject.h
static inline Py_ssize_t Py_SIZE(PyObject *ob) {
assert(Py_TYPE(ob) != &PyLong_Type);
assert(Py_TYPE(ob) != &PyBool_Type);
return _PyVarObject_CAST(ob)->ob_size;
}
사용자 정의 클래스는 그럼 어떠한 과정을 거치고 있을까요? cpython/Objects/typeobject.c
를 살펴보았습니다. 코드를 100% 정확히 이해한 것은 아니지만 sq_length
의 구현체(?)를 보면 len 을 호출하는 부분을 확인 할 수 있습니다.
...
SQSLOT(__len__, sq_length, slot_sq_length, wrap_lenfunc,
"__len__($self, /)\n--\n\nReturn len(self)."),
...
static Py_ssize_t
slot_sq_length(PyObject *self)
{
PyObject* stack[1] = {self};
PyObject *res = vectorcall_method(&_Py_ID(__len__), stack, 1); // 호출
Py_ssize_t len;
if (res == NULL)
return -1;
Py_SETREF(res, _PyNumber_Index(res));
if (res == NULL)
return -1;
assert(PyLong_Check(res));
if (_PyLong_IsNegative((PyLongObject *)res)) {
Py_DECREF(res);
PyErr_SetString(PyExc_ValueError,
"__len__() should return >= 0");
return -1;
}
len = PyNumber_AsSsize_t(res, PyExc_OverflowError);
assert(len >= 0 || PyErr_ExceptionMatches(PyExc_OverflowError));
Py_DECREF(res);
return len;
}
본 책에서는 C 코드가 전혀 나오지 않지만.. 그냥 어떤 식으로 동작하는지 확인하고 싶어 필자가 혼자 살펴본 부분입니다.
다양한 Magic method는 여기서 더 보실 수 있습니다.
이외로 !r
개념을 제대로 알고 있지 못 하여 정리하였습니다.
Python의 !r
변환 필드는 해당 객체의 repr()
형태로 변환하라는 의미입니다.
repr()
은 객체의 "공식적인" 문자열 표현을 반환하며, 가능하면 해당 객체를 다시 만들 수 있는 Python 코드 형태로 표현합니다. 즉 eval() 함수의 입력으로 사용하면 원본 객체와 동일한 값을 가진 객체를 생성할 수 있는 형태를 이야기합니다.
예시를 보면 이해가 쉽습니다.
text = "Tab\\there"
print(f"{text}") # Tab here
print(f"{text!r}") # 'Tab\\there'
주로 디버깅이나 로깅할 때 객체의 정확한 값을 표시하고 싶을 때 유용합니다. 특히 문자열에 특수 문자가 포함되어 있을 때 실제 내용을 정확히 볼 수 있습니다. Langchain의 특정 클래스의 디버깅을 위해 repr()을 사용한 기억이 떠오르네요.
긴 글 읽어주셔서 감사합니다 :)