기출노트 로고기출노트
시작하기

파이썬 b = a는 리스트를 하나 더 만드는 코드가 아니에요

a = [10, 20]
b = a
b.append(30)
print(a)
# [10, 20, 30]

b를 통해 값을 넣었는데 a로 읽어도 30이 있습니다. 리스트가 두 개였는데 하나가 다른 하나를 따라 바뀐 것이 아니에요. 같은 리스트 객체에 a와 b라는 이름이 함께 연결되어 있었습니다.

파이썬의 대입은 객체를 복사하지 않고 이름과 객체를 연결합니다.1 따라서 b = a라는 줄을 만났을 때, 새 리스트의 내용을 다시 적기보다 두 이름이 어디를 가리키는지 보면 됩니다.

대입으로 같은 리스트를 가리키는 이름과 얕은 복사에서 공유되는 내부 객체의 비교

append를 대입으로 바꾸면

앞 코드를 다시 초기화하고 이번에는 다음처럼 쓰겠습니다.

a = [10, 20]
b = a
b = b + [30]

print(a)  # [10, 20]
print(b)  # [10, 20, 30]

리스트의 +는 원소를 이어 붙인 새 리스트를 만듭니다. b는 그 새 객체에 다시 연결되고 a는 이전 객체를 가리켜요.2

append는 원래 리스트 안을 바꿨고, 위 대입은 b의 연결을 바꿨습니다. 같은 30을 추가하는 것처럼 보여도 대상이 달라요. 출력이 달라진 이유는 a를 보존하라는 특별한 규칙이 아니라 원래 객체를 바꾸었는지 여부입니다.

리스트의 +=는 이 대입으로 단순히 풀어 읽을 수 없습니다.

a = [10, 20]
b = a
b += [30]
print(a)  # [10, 20, 30]

리스트의 +=는 리스트 자체를 늘리는 연산입니다.2 ‘짧게 쓴 더하기 대입’이라는 모양만 보고 b = b + [30]과 같은 객체 변화로 처리하면 처음의 공유 관계를 놓칩니다.

copy가 만드는 것은 새 바깥 리스트예요

a = [10, 20]
b = a.copy()

print(a == b)  # True: 원소 내용이 같음
print(a is b)  # False: 다른 리스트 객체

b.append(30)
print(a)  # [10, 20]
print(b)  # [10, 20, 30]

새 바깥 리스트를 만들었으므로 b의 길이를 늘려도 a의 길이는 바뀌지 않습니다. ==와 is가 서로 다른 질문에 답한다는 것도 여기서 보입니다. 이 예제의 ==는 리스트 내용의 동등성을, is는 객체의 동일성을 비교해요.3

숫자만 들어 있을 때는 copy가 원본을 따로 보존해주는 듯 보입니다. 하지만 안에 리스트가 들어 있는 자료라면 바깥을 복사했다는 말만으로 모든 원소가 분리되었다고 할 수 없어요.

a = [[10], [20]]
b = a.copy()
b[0].append(99)

print(a)  # [[10, 99], [20]]
print(b)  # [[10, 99], [20]]

얕은 복사는 새 바깥 컨테이너에 원래 내부 원소의 참조를 넣습니다.1 a[0]과 b[0]은 같은 내부 리스트를 가리키고 있어요. b[0]의 append가 바로 그 공유한 리스트를 바꿉니다.

그다음 b.append([30])을 실행한다면 a의 바깥 리스트 길이는 그대로입니다. 이번에는 내부 첫 리스트가 아니라 바깥 b를 늘린 것이니까요. 중첩 자료에서는 어느 층의 객체를 바꾸는지까지 따라가야 합니다.

같은 내부 리스트를 공유하는 얕은 복사와 내부 리스트도 분리한 깊은 복사의 비교

내부의 변경까지 분리하려면

import copy

a = [[10], [20]]
b = copy.deepcopy(a)
b[0].append(99)

print(a)  # [[10], [20]]
print(b)  # [[10, 99], [20]]

이 자료에서는 깊은 복사가 내부 리스트까지 새로 만들었습니다. b의 내부 원소를 바꾸어도 a에 같은 변화가 보이지 않아요. Python 문서는 얕은 복사와 깊은 복사를 컨테이너 내부 항목을 복사하는 방식으로 구별합니다.1

깊은 복사는 ‘모든 참조를 각각 다른 객체로 만든다’는 뜻까지 포함하지는 않습니다. 같은 내부 리스트를 두 자리에서 참조하던 자료의 공유 관계를 복사본 안에 유지할 수 있어요.

import copy

inner = [10]
a = [inner, inner]
b = copy.deepcopy(a)

print(b[0] is b[1])  # True
print(a[0] is b[0])  # False

원본의 내부 리스트와 복사본의 내부 리스트는 갈라졌지만, 복사본 안의 두 자리는 같은 새 리스트를 가리킵니다. 문서에 설명된 memo 기록은 이미 복사한 객체를 다시 다루는 데 쓰여요.1

Footnotes

  1. Python 공식 「copy — Shallow and deep copy operations」에서 대입의 binding, 얕은·깊은 복사의 내부 처리와 deepcopy의 memo 설명을 확인했다. 코드는 본문용으로 구성해 각각 독립 실행했다. 복사 문서 ↩ ↩2 ↩3 ↩4

  2. Python 공식 「Built-in Types」의 Common Sequence Operations와 Mutable Sequence Types에서 리스트 +, copy, append, += 동작을 확인했다. 리스트 연산 ↩ ↩2

  3. Python 언어 참조 「Comparisons」의 Identity comparisons에서 is와 is not의 객체 동일성 규칙을 확인했다. 비교 규칙 ↩