질문의 배경:
파이썬에서 매개변수를 전달하는 것은 "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]
주요 특징:
.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을 기본값으로 사용하고 매 호출 시마다 명시적으로 새 객체를 생성하는 것입니다.
장점:
예측 가능성, 예상치 못한 부작용 없음, 신뢰성.
단점:
조금 더 많은 코드와 주의가 필요합니다.