본문 바로가기

C/C++

[C,C++/Python] C,C++에서 Python의 참조카운트 사용

파이썬 참조 카운트

 

 

C C++같은 언어에서는 동적 메모리 할당(malloc(), new) 및 해제(free(), delete)를 프로그래머들이 직접 관리해 줍니다.

메모리 할당을 받았으면 메모리 해제를 해줘야 하는데이게 꽤 꼼꼼한 확인이 필요합니다.

정상적으로 수행되고 있을 때는 물론에러가 발생하거나 예외가 발생했을 상황도 고려해야 한다

는 거죠

 

이썬에서는 레퍼런스가 필요할 때 마다 maoolc() free()와 같은 함수를 써서 새로운 메모리 공간을 확보하지 않고 타입에 대한 인스턴스의 레퍼런스 카운트를 통한 객체 참조 전략을 가지고 있습니다.

모든 객체는 자신이 참조된 횟수를 관리하는 카운터를 가지고 있고객체가 참조될 때마다 카운터를 하나씩 증가시키고참조가 해제 될 때 마다 레퍼런스 카운터를 하나 감소시킵니다만약 카운터가 0이 되면 그때서야 객체는 메모리 공간에서 삭제됩니다.

이런 메모리 관리 방법을 보통 가비지 콜렉션(garbage collection)이라고 부릅니다.

 

조 카운트는 파이썬에서 자동으로 관리되지만,

C확장 모듈에서는 사용자가 직접 참조 카운트의 증가/감소를 조정해야 합니다.

이와 관련된 2개의 매크로 함수가 Py_INCREF(), Py_DECREF()이며, Py_DECREF() 매크로는 참조 카운트가 0이 되었을 때 메모리를 해제하는 역할도 합니다.

 

러나언제 어떤 경우에 Py_INCREF( ) Py_DECREF( ) 매크로를 사용해야 할까요?

중요한 점은, 파이썬에서는 어떤 것도 객체를 소유할 수 없고단지 객체의 참조(reference)만을 가질 수 있다는 것입니다각 객체는 자신을 참조하는 레퍼런스의 개수를 저장하고 있어야 함은 물론이구요.

레퍼런스를 사용하고 있는 곳에서는 레퍼런스를 생성하고더 이상 필요 없을 경우 Py_DECREF( )를 호출해줘야 메모리 누수가 일어나지 않습니다.

 

퍼런스를 새로 생성하기도 하지만빌려오는(borrow) 방법도 있습니다.

아래를 보시죠.

>>> setList = [ ]            #사전 객체의 레퍼런스 생성

>>> borrow_ref = setList    #borrow_ref setList와 똑 같은 레퍼런스를 가리키고 있음

 

위와 같이 빌려온 레퍼런스를 참조 카운트를 감소시켜주면 안됩니다.

참조 카운트는 레퍼런스의 주인만이 증가감소 시킬 수 있습니다코드 상에서 레퍼런스를 마음대로 사용하고 Py_DECREF를 호출해서는 안돼요

 

 

 

레퍼런스 소유권 법칙

 

이썬 C API 함수의 인터페이스에 따라서 레퍼런스의 소유권한을 넘겨 주기도 하고 소유 권한을 빌려주기도 합니다대부분의 함수들은 레퍼런스와 함께 소유권한까지 함께 넘겨주죠.

 

PyLong_FromLong( ) Py_BuildValue( ) 같은 함수들은 레퍼런스의 소유권을 같이 넘겨주지만,

PyTuple_GetItem( ), PyList_GetItem( ), PyDict_GetItem( ), PyDict_GetItemString( )와 같은 함수들은 빌려온 레퍼런스만 넘겨줍니다특히 튜플리스트사전과 관련된 함수들은 빌려온 레퍼런스를 넘겨줍니다.

어떤 함수가 소유권을 넘겨주는지혹은 빌려주는지는 파이썬 매뉴얼에 나와 있습니다.

예를들어, Py_BuildValue( )를 보면,

PyObject*  Py_BuildValue(const char *format, ...)

Return value: New reference.

Create a new value based on a format string similar to those accepted by the PyArg_Parse*() family of functions and a sequence of values. Returns the value or NULL in the case of an error; an exception will be raised if NULL is returned.

Return value가 ‘New reference’인게 보이시죠.

 

C함수가 파이썬에서 사용될 때함수 매개변수를 통해 빌려온 레퍼런스가 전달됩니다함수로 전달된 인수들은 모두 빌려온 레퍼런스이고따라서 Py_DECREF( )를 사용하면 안돼요.

그렇기 때문에 호출한 곳에서 함수가 정상적으로 수행될 때까지 빌려온 레퍼런스의 lifetime을 보장해줘야 합니다그러기 위해서는 Py_INCREF( )를 호출해 참조 카운트를 하나 증가시켜서 사용해야 합니다.

아래 예제 처럼요.

 

tuple = PyTuple_GetItem(list1); //tuple-빌려온레퍼런스, Py_INCREF 호출할필요없음

Py_INCREF(list)                    // 참조카운트를증가시켜lifetime 보장시켜줌

long l = 0;

PyObjectitem = PyLong_FromLong((long)l); //itemnew reference

Py_DECREF(item)                              //따라서, => 참조카운터감소!!!

Py_DECREF(list)                   // list의참조카운터감소

 

 

호출한 쪽에서 소유권을 넘겨 받았으면 반드시 Py_DECREF( )를 호출해주고빌려온 레퍼런스라도 호출한 쪽에서 소유권을 유지하고 싶으면 Py_INCREF( )를 사용해야 합니다.

 

렇게 간단한 원리이지만예외란 놈이 머리를 아프게 합니다그러나 중요해요.

PyTuple_SetItem( ) PyList_SetItem( )함수는 소유권한을 빼앗아 갑니다. (함수 호출이 실패 해도 빼앗아 갑니다매뉴얼에는 아래와 같이 나와 있어요

Note : This function “steals” a reference to item and discards a reference to an item already in the list at the affected position.

“ “를 사용해서 강조하고 있네요(빨간색은 제가 한거지만).

다시 말해서소유권한을 빼앗아 가고 다시 돌려주지 않기 때문에, Py_DECREF( )를 사용하면 안돼요.

C/C++

list = PyList_GetItem(list1);

long l = 0;

PyObjectitem = PyLong_FromLong((long)l);

PyList_SetItem(listlitem);

#Py_DECREF(item) 하면 안됩니다. item 소유권을 뺏겻어요!

 

 

그리고 또 하나,

려온 레퍼런스라도 Py_INCREF( )를 반드시 해줘야 하는 경우가 있습니다호출한 쪽에서 소유권을 유지하고 싶을 경우이죠.

코드를 보시죠

C/C++

void bug(PyObjectlist)

{

        PyObjectitem = PyList_GetItem(list0);

        PyList_SetItem(list1PyLong_FromLong(0L));

        PyObject_Print(itemstdout0);

}

 

위 코드에서 함수 내부의 첫번째 코드는 list[0]에 대한 빌려온 레퍼런스를 item에 저장합니다.

그 다음 PyList_SetItem함수를 이용해 list[1] 0을 저장합니다얼핏 보기엔 정상적인 코드 입니다그러나 PyObject_Print(item, stdout, 0); 에선 에러가 발생할 수 있습니다.

 

이썬에서 리스트는 모든 아이템의 레퍼런스를 소유하고 있습니다그래서 PyList_GetItem을 호출하면 해당하는 아이템의 레퍼런스 주소값을 넘겨 주어요이때 리스트의 아이템 1번의 값을 변경하려고 하면 리스트의 아이템에 대한 모든 레퍼런스를 메모리 상에서 재배치하는 작업이 수행될 수도 있습니다!, 즉 모든 레퍼런스를 삭제하고 다시 레퍼런스를 만드는 작업을 수행할 수 있단 말입니다그렇게 되면 함수의 첫번째 라인에서 받은 빌려온 레퍼런스 item은 잘못된 메모리 주소를 가리키게 됩니다.

따라서이런 문제가 발생하지 않도록 참조 카운트를 증가시켜 소유 권한을 유지시켜 줘야 합니다.

 

지막으로,

PyObject *item = NULL;

...

Py_DECREF(item);

위 코드처럼 item의 값이 NULL 혹은 파이썬의 None일지도 모르는데 Py_DECREF를 수행하면 어떻게 될까요?

NULL일 때만 Py_DECREF를 수행하게 하려면 우선 item NULL인지 파이썬에서의 None인지를 검사해야 하는데파이썬에서 객체의 종류를 검사하는 많은 매크로 함수를 지원하지만 NULL인지를 검사하는 매크로 함수는 없습니다.

하지만, Py_XINCREF( ) Py_XDECREF( )는 내부에 NULL혹은 None인지를 검사하고 NULL일 경우 참조 카운트에 대한 작업을 무시해 버립니다.

 

위의 코드는 다음과 같이 사용하면 됩니다.

 

PyObject *item = NULL;

...

Py_XDECREF(item);

 

 

출처 :

파이썬프로그래밍위키북스