본문 바로가기

app/python

python list implementation

 

 

해당 글은 http://www.laurentluce.com/posts/python-list-implementation/ 를 번역한 글입니다. 해당 글의 로직은 python버전에 따라 다를 수 있습니다. 

 

List object C structure

 

listobject.h의 소스 파일

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;

list 오브젝트는 cpython의 위와 같은 c structure로 선언되어 있습니다.  ob_item는 list의 데이터에 대한 포인터입니다. allocated는 메모리에 할당된 슬롯의 개수입니다. list의 전체 사이즈는 ob_size로 표현되며, 크기는 아래와 같습니다.

 

- 0 <= ob_size <= allocated

- len(list) == ob_size

- ob_item == NULL일때 ob_size == allocated == 0

 

List.sort() 시엔 변화를 감지하기 위헤 -1을 일시적으로 할당합니다. 

 

 

리스트 초기화

빈 llist를 초기화하는 경우 어떤 일이 발생하는지 알아봅시다. 예를 들어 l = []입니다.

arguments: size of the list = 0
returns: list object = []
PyListNew:
    nbytes = size * size of global Python object = 0
    allocate new list object
    allocate list of pointers (ob_item) of size nbytes = 0
    clear ob_item
    set list's allocated var to 0 = 0 slots
    return list object

 

먼저 초기화를 실행하면 Objects/listobject.c의 PyList_New()를 실행합니다. 

PyObject *
PyList_New(Py_ssize_t size)
{
    PyListObject *op;

…...

    if (size <= 0)
        op->ob_item = NULL;
    else {
        op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *));
        if (op->ob_item == NULL) {
            Py_DECREF(op);
            return PyErr_NoMemory();
        }
    }

    Py_SIZE(op) = size;
    op->allocated = size;
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}

리스트의 사이즈와 할당된 슬롯의 차이가 중요합니다. 리스트의 사이즈는 len(l)와 같습니다. 

할당된 슬롯의 개수는 메모리에 할당된 것입니다. 종종 할당된 크기가 크기보다 클 수 있음을 알 수 있습니다. 새 요소가 목록에 추가될 때마다 realloc을 호출할 필요가 없습니다.

 

 

리스트 추가

L.append(1) , 리스트에 정수를 추가합니다. 

arguments: list object, new element
returns: 0 if OK, -1 if not
app1:
    n = size of list
    call list_resize() to resize the list to size n+1 = 0 + 1 = 1
    list[n] = list[0] = new element
    return 0

현재 l= []로 원소가 하나도 들어 있지 않습니다. 여기에서 리스트에 List의 사이즈를 가져온 후 list_resize()를 통해 슬롯 사이즈를 늘린 후 set_item()을 통해 새로운 엘리멘트를 할당합니다. 여기서 눈여겨볼 것은 list_resize()입니다. 

 

Objects/listobject.c 의 list_resize()

arguments: list object, new size
returns: 0 if OK, -1 if not
list_resize:
    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) = 3
    new_allocated += newsize = 3 + 1 = 4
    resize ob_item (list of pointers) to size new_allocated
    return 0


/* This over-allocates proportional to the list size, making room
* for additional growth.  The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
* sequence of appends() in the presence of a poorly-performing
* system realloc().
* The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
*/

 

list_resize()를 보자. list_resize()의 로직은 list_resize를 너무 많이 호출하지 않도록 메모리를 과도하게 할당합니다. list의 할당 패턴은 다음과 같습니다. 0, 4, 8, 16, 25, 35, 46, 58, 72, 88,…

4개의 슬롯을 새롭게 할당한 후 첫 번째 슬롯엔 1이 들어갑니다. 


만약 n = 1000 인 경우 list의 크기는 27번 조정됩니다. 성장 패턴은 다음과 같습니다 : 4, 8, 16, 25, 35, 46, 58, 72, 88, 106, 126, 148, 173, 201, 233, 269, 309, 354, 405, 462, 526, 598 , 679, 771, 874, 990, 1120입니다. 이는 realloc에 대한 27번의 호출을 의미합니다. 


 

다음 다이어그램은 l[0]은 1이 할당되어 있습니다. 점선의 사각형은 아직 사용하지 않은 슬롯입니다. 

Append의 실행 시간은 O(1)입니다.

 

l.append (2) 요소를 하나 더 추가합니다. list_resize는 n + 1 = 2로 호출되지만 할당된 크기가 4이므로 더 많은 메모리를 할당할 필요가 없습니다. l.append (3), l.append (4)의 정수를 2 개 더 추가해도 마찬가지입니다. 다음 다이어그램은 우리가 지금까지 추가된 것을 보여줍니다.

 

 

 

Insert

위치 1에 새로운 정수 5를 삽입할 때 l.insert(1, 5),  내부적으로 어떤 일이 발생하는지 살펴보겠습니다. 

Ins1()을 호출합니다. 

arguments: list object, where, new element
returns: 0 if OK, -1 if not
ins1:
    resize list to size n+1 = 5 -> 4 more slots will be allocated
    starting at the last element up to the offset where, right shift each element
    set new element at offset where
    return 0
static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
    Py_ssize_t i, n = Py_SIZE(self);
    PyObject **items;

 …………….. . . . . . . . . . .

   if (where < 0) {
        where += n;
        if (where < 0)
            where = 0;
    }

    if (where > n)
        where = n;

    items = self->ob_item;

    for (i = n; --i >= where; )
        items[i+1] = items[i];
        
    Py_INCREF(v);
    items[where] = v;
    return 0;
}

 

insert 되는 해당 요소의 자리 이후의 요소들을 전부 옮긴 후 마지막에 insert()의 요소를 해당 슬롯에 넣어줍니다. 

 

 

 

점선의 사각형은 새롭게 할당된 슬롯으로 아직 사용하지 않는 부분입니다. 8개의 슬롯이 할당되었지만, 리스트의 사이즈는 5입니다. 

Insert시 실행 시간은 O(n)입니다. 

 

 

 

 

POP

마지막 요소를 pop 할 경우 l.pop() , listpop()을 호출합니다. listpop()에서 list_resize()를 호출하며, 크기가 할당된 절반보다 작으면 list_ass_slice()를 호출하여 슬롯을 축소합니다.

 

arguments: list object
returns: element popped
listpop:
    if list empty:
        return null
    resize list with size 5 - 1 = 4. 4 is not less than 8/2 so no shrinkage
    set list object size to 4
    return last element

Pop 연산의 실행 시간은 O(1)입니다. 

 

 

 

l.pop()을 통해 슬롯 4번째가 여전히 정수 4를 가리키는 걸 볼 수 있지만 중요한 것은 현재 크기가 4입니다. 

하나 더 요소를 pop()합니다. List_resize()에서 size - 1 = 4 - 1 = 3은 할당된 슬롯의 절반보다 작으므로 슬롯은 6개로 줄어들로 목록의 크기는 3이 됩니다. 

 

 

 

 

Remove

특정 요소를 제거하기 위해 l.remove(5)를 호출하면 listremove()를 호출합니다.

arguments: list object, element to remove
returns none if OK, null if not
listremove:
    loop through each list element:
        if correct element:
            slice list between element's slot and element's slot + 1
            return none
    return null

 

요소를 지우고 리스트를 자르기 위해 list_ass_slice()가 호출되며, 이 작업이 어떻게 동작하는지  흥미롭습니다. 

이 작업은 오프셋 1의 위치에 오프셋 2가 들어가며 (5의 위치에 2가 들어감) 1에 위해 했던 5의 요소는 삭제됩니다.

arguments: list object, low offset, high offset
returns: 0 if OK
list_ass_slice:
    copy integer 5 to recycle list to dereference it
    shift elements from slot 2 to slot 1
    resize list to 5 slots
    return 0

 

삭제 연산의 실행 시간은 o(n)입니다. 

 

 

 

 

 

 

 

 

'app > python' 카테고리의 다른 글

wsgi를 왜 쓰나요  (3) 2020.03.31
Python dictionary implementation  (1) 2020.03.21
Python string objects implementation  (0) 2020.03.19
Python integer objects implementation  (0) 2020.03.18
python hash && cache  (0) 2020.03.11