Python 함수 기본값으로 가변 객체 사용 시 주의점: 방심하면 생기는 복잡한 버그들!

Python에서 함수의 기본값으로 가변 객체(예: 리스트, 딕셔너리, 집합)를 사용할 때는 특별히 주의해야 합니다. 기본값은 함수 정의 시점에 한 번만 평가되기 때문에, 가변 객체를 기본값으로 사용하면 모든 함수 호출에서 동일한 객체를 공유하게 됩니다.

이 동작은 의도치 않은 동작을 유발할 수 있으며, 특히 코드가 복잡해질수록 디버깅이 어려운 버그로 이어질 수 있습니다. 이번 글에서는 이를 방지하기 위한 방법과 실제 코드에서 자주 발생하는 사례를 살펴봅니다.


1. 기본값으로 가변 객체를 사용했을 때 생기는 문제

Python에서 함수 기본값으로 리스트 같은 가변 객체를 설정하면, 함수가 호출될 때마다 새로운 객체가 생성되지 않습니다. 대신, 동일한 객체가 모든 호출에서 재사용됩니다. 이를 이해하기 위해 아래 코드를 살펴봅시다.

def append_to_list(value, my_list=[]):
    my_list.append(value)
    return my_list

# 호출
print(append_to_list(1))  # [1]
print(append_to_list(2))  # [1, 2]
print(append_to_list(3))  # [1, 2, 3]

결과

[1]
[1, 2]
[1, 2, 3]

설명

  • my_list는 기본값으로 빈 리스트([])를 갖습니다.
  • 하지만 함수 호출마다 새로운 리스트가 생성되지 않고, 기존 리스트가 재사용됩니다.
  • 이로 인해 호출마다 값이 기존 리스트에 추가되어, 예상치 못한 결과를 초래합니다.

2. 이런 문제가 발생하는 이유: Python 기본값 동작 원리

Python에서 함수의 기본값은 함수 정의 시점에 한 번만 평가됩니다. 즉, 가변 객체를 기본값으로 설정하면, 그 객체는 모든 함수 호출에서 공유됩니다.

비유로 이해하기

기본값으로 가변 객체를 설정하면 새로운 물건을 받는 게 아니라, 항상 같은 창고에서 물건을 꺼내 쓰는 것과 같습니다. 만약 창고에 물건이 추가되면, 다음 호출에서도 그 변경된 상태가 유지됩니다.


3. 해결 방법: 기본값으로 None 사용하기

가변 객체를 기본값으로 설정하고 싶다면, 기본값에 None을 사용하고, 함수 내부에서 객체를 생성하는 방법이 가장 안전합니다.

def append_to_list(value, my_list=None):
    if my_list is None:
        my_list = []  # 새로운 리스트 생성
    my_list.append(value)
    return my_list

# 호출
print(append_to_list(1))  # [1]
print(append_to_list(2))  # [2]
print(append_to_list(3))  # [3]

결과

[1]
[2]
[3]

설명

  • 기본값으로 None을 설정하고, 함수 내부에서 리스트를 생성합니다.
  • 각 함수 호출마다 새로운 객체가 생성되기 때문에, 독립된 값을 반환합니다.
  • 이런 방식은 가변 객체를 안전하게 기본값으로 사용할 수 있는 가장 널리 쓰이는 패턴입니다.

4. 실전 예제: 가변 객체 기본값으로 생긴 버그와 해결 방법

아래 코드는 기본값으로 딕셔너리를 사용하는 경우에 발생하는 문제를 보여줍니다.

def add_to_dict(key, value, my_dict={}):
    my_dict[key] = value
    return my_dict

# 호출
print(add_to_dict("a", 1))  # {'a': 1}
print(add_to_dict("b", 2))  # {'a': 1, 'b': 2}

결과

{'a': 1}
{'a': 1, 'b': 2}

문제 설명

  • my_dict가 모든 호출에서 동일한 객체를 공유하기 때문에, 함수 호출이 독립적이지 않습니다.
  • 두 번째 호출에서 딕셔너리가 초기화되지 않고, 이전 상태를 유지합니다.

해결 방법: None 사용

def add_to_dict(key, value, my_dict=None):
    if my_dict is None:
        my_dict = {}  # 새로운 딕셔너리 생성
    my_dict[key] = value
    return my_dict

# 호출
print(add_to_dict("a", 1))  # {'a': 1}
print(add_to_dict("b", 2))  # {'b': 2}

결과

{'a': 1}
{'b': 2}

5. 실제 프로젝트에서 주의해야 할 사례

사례 1: API 요청에서 데이터 누적 문제

API 요청에 필요한 파라미터를 딕셔너리로 기본값으로 설정하면, 여러 요청 간 데이터가 누적되어 의도하지 않은 요청을 보내게 될 수 있습니다.

잘못된 코드

def api_request(params={}):
    params["token"] = "secure_token"
    return params

# 호출
print(api_request({"user": "Alice"}))  # {'user': 'Alice', 'token': 'secure_token'}
print(api_request({"user": "Bob"}))    # {'user': 'Bob', 'token': 'secure_token'}

해결 코드

def api_request(params=None):
    if params is None:
        params = {}
    params["token"] = "secure_token"
    return params

# 호출
print(api_request({"user": "Alice"}))  # {'user': 'Alice', 'token': 'secure_token'}
print(api_request({"user": "Bob"}))    # {'user': 'Bob', 'token': 'secure_token'}

사례 2: 데이터 분석에서 리스트 기본값 문제

데이터를 필터링하여 결과를 리스트로 저장할 때, 기본값으로 리스트를 사용하면 값이 누적될 수 있습니다.

잘못된 코드

def filter_data(data, results=[]):
    for value in data:
        if value > 10:
            results.append(value)
    return results

# 호출
print(filter_data([5, 15, 25]))  # [15, 25]
print(filter_data([30, 40]))     # [15, 25, 30, 40]

해결 코드

def filter_data(data, results=None):
    if results is None:
        results = []
    for value in data:
        if value > 10:
            results.append(value)
    return results

# 호출
print(filter_data([5, 15, 25]))  # [15, 25]
print(filter_data([30, 40]))     # [30, 40]

6. 요약: 가변 객체 기본값 사용 시 반드시 기억할 점

  1. 가변 객체 기본값의 위험:
    • 함수 호출마다 동일한 객체를 공유하여, 의도하지 않은 결과를 초래할 수 있습니다.
  2. 안전한 패턴:
    • 기본값으로 None을 사용하고, 함수 내부에서 가변 객체를 초기화합니다.
  3. 실전에서 발생할 수 있는 버그:
    • 리스트, 딕셔너리, 집합 같은 가변 객체의 기본값을 사용할 때 예상치 못한 동작이 발생할 수 있습니다.

Python에서 함수 기본값으로 가변 객체를 사용할 때 발생할 수 있는 문제는 처음에는 잘 보이지 않을 수 있습니다. 하지만 코드가 복잡해지면 치명적인 버그로 이어질 수 있으니, 항상 None 패턴을 사용해 안전하게 코드를 작성하세요!

+ Recent posts