[Python] 함수에서 다른 함수 변수 접근하기(c 포인터처럼 사용해보기)

Python에서 list를 key로 받는 dictionary를 만들고자 시도를 하다가, 포인터 개념을 이용해 객체의 주소를 key로 사용하면 key로 객체에 바로 접근할 수 있지 않을까 하는 생각에서 출발했습니다. 물론 list를 tuple로 변경해 immutable하게 만들어 dictionary key로 사용할수도 있겠지만 iterable한 객체를 key로 사용한다는 게 별로 마음에 들지 않았습니다.

원하는만큼 Python에서 포인터를 구현해 사용해보지는 못했지만, 공부했던 내용을 기록합니다. 공부기록이기 때문에 더 찾아보지 못한 내용이나 잘못된 개념이 있을 수 있습니다.


C / C++을 알고 있다면 다른 함수의 변수에 접근하는 것을 간단합니다. 포인터를 사용하면 됩니다.

Get value with c pointer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

void get_object_by_id(int *id) {
    printf("%s", id);

}

int main() {
    int *id;
    char string[] = "This is local variable";
    id = &string;
    get_object_by_id(id);
    return 0;
}

// This is local variable

포인터는 변수가 아닌 값의 주소로 값에 접근하는 개념입니다. 파이썬에도 주소는 존재하기 때문에 이론적으로는 포인터와 같은 개념을 사용할 수 있습니다.


Get value in python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import ctypes

def get_object_by_id(object_id):
    obj = ctypes.cast(object_id, ctypes.py_object).value
    print(obj)
    return

def main():
    string = 'This is local variable'
    object_id = id(string)
    get_object_by_id(object_id)

main()

# This is local variable

ctypes 라이브러리를 사용하면 id로 메모리에 바로 접근할 수 있습니다. 그렇다면 값을 변경할수도 있을까요?

포인터 측면에서 생각해보면, 메모리에 주소로 바로 접근하기 때문에 함수의 지역여부와 관계없이 주소만 알고 있다면 값에 접근할수도 바꿀수도 있습니다.


Change value with pointer in C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

void get_object_by_id(int *id) {
    *id = 56;
}

int main() {
    int *id;
    int number = 10;
    id = &number;
    printf("%d\n", *id);
    get_object_by_id(id);
    printf("%d\n", *id);
    return 0;
}

# 10
# 56

그렇다면 python에서도 동일하게 작동할까요?


Change value with ctypes library in Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import ctypes

def get_object_by_id(object_id):
    ctypes.cast(object_id, ctypes.py_object).value = 56


def main():
    num = 10
    object_id = id(num)
    get_object_by_id(object_id)
    after_change = ctypes.cast(object_id, ctypes.py_object).value
    print(after_change)

main()

# 10

python에서는 값이 변경되지 않았습니다. 왜일까요?

python에서 int는 immutable이기 때문입니다. python에서 immutable 객체(int, str …)에 새로운 값이 할당되면 값이 변경되는 것처럼 보이지만, 새로운 메모리 공간에 값이 할당된다는 점을 알고 있다면 왜 값이 변경되지 않았는지 이해할 수 있습니다.


1
2
3
4
5
6
7
8
a = 1
print(f'value : {a}, id : {id(a)}')
a = 3
print(f'value : {a}, id : {id(a)}')

# value : 1, id : 140734177158944
# value : 3, id : 140734177159008

새로운 값을 할당하면, 주소도 변경됩니다.

파이썬에서 포인터처럼 주소를 이용해 값을 변경하려고 하더라도, 그 메모리 주소에 해당하는 공간이 아닌 전혀 다른 공간에 값이 할당됩니다. 즉, 저 56은 참조되지 않은 상태로 메모리 어딘가에 할당되기 때문에 파이썬 Reference count에 의해 메모리에서 삭제될 확률이 아주 높습니다.


그렇다면 mutable 객체의 값은 변경할 수 있을까요? 변경 전후의 id와 값을 비교해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import ctypes

def get_object_by_id(object_id):
    ctypes.cast(object_id, ctypes.py_object).value.append('append value in another function!')


def main():
    arr = ['local variable']
    object_id = id(arr)
    print(f'before >> value : {arr}, id : {id(arr)}')
    get_object_by_id(object_id)
    after_change = ctypes.cast(object_id, ctypes.py_object).value
    print(f'after >> value : {arr}, id : {id(arr)}')

main()

# before >> value : ['local variable'], id : 2357128055680
# after >> value : ['local variable', 'append value in another function!'], id : 2357128055680

mutable 객체는 다른 함수에서도 접근해 값을 변경할 수 있습니다.

비록 이 개념을 현재 업무에 사용하는 코드에 반영하지는 못했습니다. 그렇지만 언어에 국한된 개념이 아닌, 코드가 메모리 내부에서 어떻게 작동하는가에 대해 생각해 볼 수 있었습니다.


주의사항

이 개념을 사용하지 못한 이유는 Python은 메모리를 프로그래머가 조작하지 않는 unmanaged 언어이기 때문입니다.

파이썬은 Reference counting, GC를 이용해 자동으로 메모리 관리를 합니다. 프로그래머가 직접 메모리를 관리하는 것보다는 효율성이 떨어질 수 있기 때문에 자동 메모리 관리를 해제하고 Python을 사용하는 경우도 있지만(Python GC를 사용하지 않는 instagram), 쉽지는 않은 일입니다.

자동으로 메모리가 관리된다는 말은 내가 작성한 코드의 프로세스가 끝날때까지 내가 변수를 이용해 접근하고자 하는 메모리 공간에 그 값이 남아있을지 100% 보장할 수 없다는 의미이기도 합니다. 쓰레기 값으로 대체될 가능성이 있다고 생각했습니다.

이는 서버, OS까지 고려해야 하는 문제이기 때문에 일개 프로그래머가 조작할 수 없는 영역이라 생각했습니다.