파이썬에서의 참조와 값의 전달: 포인터 없는 언어의 깊은 이해

파이썬은 초심자에게 매우 친절한 언어로 알려져 있지만, 그 이면에는 깊이 있는 메모리 모델과 객체 지향 구조가 존재한다. 특히 함수 호출 시의 인자 전달 방식은 C/C++의 포인터 개념에 익숙한 사람에게 혼란을 줄 수 있다. “파이썬에는 포인터가 없다”고 알려져 있지만, 실질적으로 파이썬의 참조 기반 처리 방식은 포인터가 제공하는 기능 상당 부분을 대체하거나 추상화한다. 이 글에서는 그 내부 메커니즘을 해부하고, 실전 프로그래밍에서 이를 어떻게 이해하고 활용할 수 있는지를 심층적으로 고찰해본다.


함수 호출 시 값은 어떻게 전달되는가?

파이썬에서는 모든 것이 객체이며, 변수는 이 객체에 대한 ‘이름표(name tag)’ 역할을 한다. 이는 파이썬의 모든 함수 인자 전달 방식이 **”객체의 참조(reference) 전달”**이라는 사실을 이해하는 데 핵심이다.

def change(x):
    x = 10

a = 5
change(a)
print(a)  # 여전히 5

이 예제에서 변수 a는 정수 5를 가리키고 있다. change() 함수는 x를 통해 이 값을 받지만, 정수는 불변(immutable) 객체이기 때문에 x = 10이라는 할당은 새로운 객체를 생성하여 x에 연결시킬 뿐, 원래 a와는 아무 관련이 없어진다. 이는 마치 C에서 값이 복사되어 전달된 것처럼 동작한다.


참조에 의한 “수정”이 가능한 경우

가변(mutable) 객체, 예를 들어 리스트나 딕셔너리 등은 다르게 작동한다.

def modify_list(lst):
    lst.append(100)

my_list = [1, 2, 3]
modify_list(my_list)
print(my_list)  # [1, 2, 3, 100]

이 경우 lstmy_list는 동일한 리스트 객체를 참조한다. 함수 내부에서 리스트에 원소를 추가하면, 외부에서도 그 결과를 볼 수 있다. 이것이 참조의 실질적인 위력이다. 이 행동은 마치 C에서 포인터를 이용해 배열을 조작하는 것과 유사하다. 하지만 차이점은 분명히 존재한다. 파이썬 개발자는 메모리 주소나 포인터 산술 연산 등을 직접 다루지 않는다. 참조는 언어가 자동으로 처리한다.


*args**kwargs는 포인터인가?

*args는 포인터와 헷갈릴 수 있지만 완전히 다른 개념이다. *args는 함수 정의에서 가변 positional 인자를 튜플로 수집하는 문법일 뿐이며, 메모리 주소를 직접 넘기는 것이 아니다. 다만 args 내부에 들어있는 객체들이 모두 참조를 통해 접근된다는 점에서는 일종의 ‘포인터 컬렉션’처럼 행동한다.

def func(name, *args):
    for arg in args:
        print(f"{name}: {arg}")

이러한 문법은 함수 인터페이스를 유연하게 설계할 수 있게 해주지만, 포인터처럼 메모리 조작이 가능한 구조는 아니다.


객체 ID와 참조 비교

파이썬에서 참조의 실체를 확인하려면 id() 함수를 활용할 수 있다. 이는 객체의 고유 식별자(일종의 메모리 주소)를 반환한다.

a = [1, 2]
b = a
print(id(a) == id(b))  # True

이것은 ba와 동일한 리스트 객체를 참조하고 있음을 보여준다. 새로운 리스트를 할당하면 참조가 바뀐다.

b = [1, 2]
print(id(a) == id(b))  # False

얕은 복사와 깊은 복사: 참조의 함정

참조 기반 언어에서 가장 흔히 발생하는 문제는 복사의 오해다. copy.copy()는 객체의 얕은 복사를 수행하여 상위 객체는 복제하지만 하위 객체는 그대로 참조한다. 반면 copy.deepcopy()는 모든 계층을 재귀적으로 복사한다.

import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)

얕은 복사를 이해하지 못하면, 리스트 안의 리스트처럼 중첩된 구조에서 원치 않는 참조 공유가 발생할 수 있다. 이는 종종 디버깅을 어렵게 만드는 요인이다.


정리하며

파이썬은 포인터 연산이 없는 대신, 모든 객체를 참조로 처리함으로써 메모리 모델을 단순화했다. 이는 개발자에게 불필요한 메모리 관리를 강요하지 않고도 포인터의 주요 기능을 사용할 수 있게 한다. 하지만 참조의 특성을 이해하지 못하면 예기치 않은 동작을 경험할 수 있다.

결국 파이썬의 변수는 ‘값’을 저장하지 않는다. 변수는 단지 객체를 가리키는 이름이며, 그 객체가 어디에 있고 무엇을 하는지는 파이썬이 관리한다. 이것이 바로 고급 언어로서 파이썬이 가진 진정한 추상화의 힘이다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다