전문가를 위한 파이썬 - 1장

L-cloud·1일 전
1

본 글은 전문가를 위한 파이썬 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()을 사용한 기억이 떠오르네요.

긴 글 읽어주셔서 감사합니다 :)

profile
내가 배운 것 정리

0개의 댓글