이번 편의 목표: 지역변수·가변 객체·얕은 복사·기본값 함정을 이해합니다.
리스트를 다른 변수에 담았는데 한쪽을 수정하자 다른 쪽도 바뀌는 일이 있습니다. 파이썬이 멋대로 값을 바꾼 것이 아닙니다. 두 이름이 같은 객체를 가리키고 있었던 것입니다. 이번 편은 나중에 찾기 어려운 오류를 예방하는 데 중요합니다.
대입은 복사 명령이 아니다
original = [1000, 2000]
alias = original
alias.append(3000)
print(original)
copied = original.copy()
copied.append(4000)
print(original)
print(copied)
def add_amount(amounts, amount):
amounts.append(amount)
add_amount(original, 5000)
print(original)`alias = original`은 새 리스트를 만들지 않습니다. 같은 리스트에 이름 하나가 더 붙습니다. 따라서 첫 출력은 `[1000, 2000, 3000]`입니다. `.copy()`는 바깥 리스트를 새로 만들어, 그 리스트에 추가한 `4000`은 원본에 들어가지 않습니다.
함수에 리스트를 넘겼을 때도 같습니다. 매개변수 `amounts`가 같은 리스트를 가리키기 때문에 `.append()`가 호출자의 자료를 바꿉니다. 반대로 함수 안에서 `amounts = []`라고 이름을 새 리스트에 연결하는 것은 호출자의 이름을 다시 연결하는 일이 아닙니다. 객체를 변경하는 것과 이름을 재대입하는 것은 구분해야 합니다.
얕은 복사는 어디까지 복사할까
`.copy()`는 얕은 복사입니다. 리스트 안에 딕셔너리가 들어 있으면 안쪽 딕셔너리들은 여전히 공유됩니다. 따라서 복사한 목록의 `copied[0]["amount"]`를 수정하면 원본 행도 바뀔 수 있습니다. 안쪽까지 독립적으로 복사해야 하는 경우 `copy.deepcopy()`를 검토합니다.
모든 곳에서 깊은 복사를 하는 것이 정답은 아닙니다. 큰 자료에서는 비용이 들고, 일부 객체는 복사 의도를 따로 정해야 합니다. 조회 결과를 수정하지 않는 구조나, 필요한 행만 새 딕셔너리로 만드는 구조도 가능합니다.
함수 안의 이름과 기본값
함수 안에서 대입한 이름은 보통 그 함수의 지역변수입니다. 바깥의 같은 이름과는 별개입니다. 전역변수를 직접 고치는 `global` 문법도 있지만, 이 연재에서는 인수를 받고 결과를 반환하는 방식으로 자료 흐름을 드러냅니다.
또 하나의 함정은 `def collect(item, items=[]):`처럼 리스트를 기본값으로 사용하는 것입니다. 기본값은 호출할 때마다 새로 만들어지지 않고 함수 정의 시 만들어집니다. 서로 다른 호출에서 같은 목록을 공유할 수 있습니다. 호출마다 새 목록이 필요하면 `None`을 기본값으로 받고 안에서 만듭니다.
직접 해 보기
항목을 하나 받아 새 목록에 담는 함수를 만드세요. 목록을 명시해서 넘기면 그 목록에 추가하고, 생략하면 호출마다 새 목록을 만드세요.
해설
def collect(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(collect("커피"))
print(collect("점심"))
shared = []
collect("버스", shared)
collect("지하철", shared)
print(shared)결과는 `['커피']`, `['점심']`, `['버스', '지하철']`입니다. `None` 여부에는 `is None`을 사용합니다. 일반적인 문자열·숫자의 값 비교에는 `==`를 사용하세요.
COMMENTS
댓글 0
이 글에 대한 의견이나 질문을 남겨 주세요.
등록된 댓글이 없습니다.