프로그래밍파이썬 개발자 / 데이터 엔지니어

파이썬에서 변경 가능한 객체(예: 리스트 또는 딕셔너리)를 함수에 전달할 때 어떤 일이 발생합니까? 함수 내외부에서 예상치 못한 변경을 어떻게 피할 수 있습니까?

Hintsage AI 어시스턴트로 면접 통과

답변.

질문의 배경:

파이썬에서 매개변수를 전달하는 것은 "call by object reference" 원칙을 구현합니다(가끔은 call by sharing이라고도 합니다). 이는 함수 내부의 변수가 외부에서 전달된 인수와 같은 메모리의 동일한 객체를 가리키기 시작함을 의미합니다.

문제:

함수가 전달된 변경 가능한 객체(예: 리스트 또는 딕셔너리)를 수정하면 함수 외부에서도 변경 사항이 보입니다. 이는 함수가 입력 데이터를 변경하지 않을 것이라고 예상하는 경우 특히 추적하기 어려운 버그를 초래할 수 있습니다.

해결책:

부작용을 피하기 위해 함수 내부에서 객체의 복사본을 만들거나 변경 불가능한 데이터 구조를 사용하는 것이 좋습니다. 복사에는 표준 메서드를 사용합니다(예: 리스트에 대한 list.copy(), 딕셔너리에 대한 dict.copy() 또는 copy.deepcopy()).

코드 예:

def append_one(xs): xs.append(1) return xs lst = [0] append_one(lst) print(lst) # [0, 1] # 변경을 피하려면? 복사를 만듭니다: def safe_append_one(xs): ys = xs.copy() ys.append(1) return ys lst2 = [0] safe_append_one(lst2) print(lst2) # [0]

주요 특징:

  • 변경 가능한 객체를 전달하면 함수 내부와 외부 모두에서 상태를 변경할 수 있습니다.
  • 이를 피하기 위해 데이터 복사(shallow/deep copy)를 사용합니다.
  • 변경 불가능한 객체는 이러한 변경으로부터 보호됩니다.

속임수 질문.

.copy()를 사용하여 리스트 복사본이 원본 리스트와 완전히 독립적이라는 확신을 가질 수 있습니까?

아니요 — .copy()는 얕은 복사본을 생성합니다. 내부에 변경 가능한 객체가 포함된 경우, 그 변경 사항은 원본에서도 보입니다.

import copy lst = [[1, 2], [3, 4]] shallow = lst.copy() shallow[0][0] = 42 print(lst) # [[42, 2], [3, 4]] deep = copy.deepcopy(lst) deep[0][0] = 100 print(lst) # [[42, 2], [3, 4]]

입력에 기반한 새 객체를 반환하는 것이 원본 변경이 없다는 보장이 됩니까?

항상 그런 것은 아닙니다. 새 객체 내부에 원본의 일부(예: 내부 중첩 리스트에 대한 참조)가 사용되면 원본 객체가 변경될 수 있습니다.

def duplicate_list(xs): return xs * 2 lst = [[1], [2]] res = duplicate_list(lst) res[0][0] = 999 print(lst) # [[999], [2]]

변경 가능한 객체에 대한 기본값으로 사용되는 인자가 함수의 여러 번 호출 시 문제를 일으킬 수 있습니까?

네 — 기본값은 함수 정의 시 한 번만 평가됩니다.

def add_item(item, container=[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]

일반적인 오류 및 안티 패턴

  • 사용자에게 알리지 않고 함수 내부에서 전달된 변경 가능한 객체를 수정합니다.
  • 중첩 데이터 구조에 대해 얕은 복사를 사용하는 것(변이하는 중첩 객체와 관련된 오류).
  • 함수 인수의 기본값으로 변경 가능한 객체를 사용하는 것.

실제 사례

부정적인 사례

구성 파일 처리를 위한 라이브러리에서 기본값으로 리스트를 사용하여 서로 다른 함수 호출 간에 요소가 누적되는 문제가 발생했습니다. 이 동작은 예측할 수 없었고 오랜 시간 동안 발견되지 않았습니다.

장점:

재호출에 있는 코드가 줄어들고 메모리 사용이 눈에 띄게 절약됩니다.

단점:

암묵적 동작, 디버깅의 어려움, 장기적인 오류.

긍정적인 사례

None을 기본값으로 사용하고 매 호출 시마다 명시적으로 새 객체를 생성하는 것입니다.

장점:

예측 가능성, 예상치 못한 부작용 없음, 신뢰성.

단점:

조금 더 많은 코드와 주의가 필요합니다.