Python 함수의 기본 인자로 가변 객체 사용 시 발생하는 부작용과 해결책

Python에서 함수의 기본 매개변수로 리스트나 딕셔너리 같은 가변 객체(Mutable Object)를 사용할 때 예상치 못한 동작이 발생할 수 있습니다. 이 글에서는 해당 현상의 원인과 올바른 사용 패턴, 그리고 이 특성을 의도적으로 활용하는 방법에 대해 다룹니다.

예상치 못한 동작 현상

다음과 같이 기본 인자로 빈 리스트를 할당하는 함수를 정의하고 여러 번 호출해 보겠습니다.

def accumulate_data(data_store=[], item=10):
    data_store.append(item)
    return data_store

print(accumulate_data())  # [10]
print(accumulate_data())  # [10, 10]
print(accumulate_data())  # [10, 10, 10]

동일한 방식으로 인자 없이 함수를 호출했음에도 불구하고, 반환되는 리스트의 길이가 계속 증가하는 것을 확인할 수 있습니다. 이는 직관적으로 기대하는 "매 호출마다 새로운 빈 리스트가 생성될 것"이라는 예상과 완전히 다릅니다.

원인 분석: 함수 객체의 생성 시점

Python에서 함수는 일급 객체(First-class object)입니다. 즉, 정수나 문자열과 마찬가지로 변수에 할당되거나 인자로 전달될 수 있는 독립적인 객체입니다.

def 키워드는 단순한 선언이 아닌 실행문입니다. 인터프리터가 def 문을 만나면 메모리에 함수 객체를 생성하고, 전역 네임스페이스에 해당 함수 이름을 바인딩합니다. 이때 함수의 내부 로직은 실행되지 않지만, 기본 매개변수들은 함수 객체가 생성되는 시점에 단 한 번만 초기화됩니다.

초기화가 완료되면 함수 객체의 __defaults__ 속성에 기본 인자들이 바인딩됩니다. 앞서 예시에서 data_store는 함수 정의 시점에 생성된 단 하나의 리스트 객체를 계속 참조하게 됩니다. 함수를 호출할 때마다 인자를 생략하면, 매번 동일한 리스트 객체에 접근하여 요소를 추가하게 되므로 리스트가 계속 누적되는 것입니다.

반면, 호출 시 명시적으로 새로운 리스트를 전달하면 어떻게 될까요?

print(accumulate_data(data_store=[100, 200]))  # [100, 200, 10]

이 경우 data_store는 초기화 시점의 리스트가 아닌, 호출 시점에 전달된 새로운 리스트 객체를 참조하게 되므로 예상한 결과가 출력됩니다.

올바른 사용 패턴: None 활용

가변 객체를 기본 인자로 사용할 때의 부작용을 방지하려면, 기본값으로 None을 지정하고 함수 내부에서 동적으로 객체를 생성하는 패턴을 사용해야 합니다.

def accumulate_data(data_store=None, item=10):
    if data_store is None:
        data_store = []
    data_store.append(item)
    return data_store

print(accumulate_data())  # [10]
print(accumulate_data())  # [10]
print(accumulate_data())  # [10]

이 방식을 사용하면 인자를 생략하고 호출할 때마다 data_storeNone으로 평가되어 함수 내부에서 매번 새로운 빈 리스트가 생성됩니다. 따라서 각 호출은 서로 독립적인 상태를 유지하게 됩니다.

특성의 의도적 활용: 상태 캐싱

그렇다면 이 특성을 아예 사용하지 말아야 할까요? 반드시 그렇지는 않습니다. 함수 객체가 기본 인자의 상태를 유지한다는 특성을 역이용하면, 별도의 전역 변수나 클래스 없이도 간단한 캐싱(Caching)이나 호출 횟수 기록 기능을 구현할 수 있습니다.

def fetch_with_cache(url, cache={}):
    if url not in cache:
        # 실제 네트워크 요청 대신 더미 데이터 사용
        cache[url] = f"Response from {url}"
    return cache[url]

print(fetch_with_cache("api.example.com/data"))
print(fetch_with_cache("api.example.com/data"))  # 캐시된 결과 반환
print(fetch_with_cache.__defaults__)  # ({'api.example.com/data': 'Response from api.example.com/data'},)

위 코드에서 cache 딕셔너리는 함수가 정의될 때 생성되어 호출 간에 상태를 공유합니다. 이를 통해 이전에 요청한 URL의 결과를 메모리에 저장하고 재사용하는 간단한 메모이제이션(Memoization) 패턴을 구현할 수 있습니다.

태그: python MutableObjects DefaultArguments FirstClassObject FunctionDefaults

5월 23일 22:50에 게시됨