배열 뷰와 얕은 복사 — 수정 전파 주의
이 토픽을 마치면
NumPy에서 뷰(view)와 복사(copy)의 차이를 이해하고, 슬라이싱이 뷰를 반환하는 이유를 설명할 수 있으며, 의도하지 않은 수정 전파를 방지할 수 있습니다.
놀라운 동작
import numpy as np
original = np.array([1, 2, 3, 4, 5])sliced = original[1:4]
sliced[0] = 99
print(sliced) # [99 3 4]print(original) # [ 1 99 3 4 5] ← 원본도 바뀜!sliced만 수정했는데 original도 바뀌었습니다. 이것은 버그가 아니라 설계입니다. NumPy 슬라이싱은 데이터를 복사하지 않고 **같은 메모리를 공유하는 뷰(view)**를 반환합니다.
뷰(View)란
뷰는 같은 데이터를 다른 관점으로 보는 것입니다.
메모리: [1] [2] [3] [4] [5]
↑ ↑ ↑ ↑ ↑
original: [0] [1] [2] [3] [4]
sliced = original[1:4]
↑ ↑ ↑
sliced: [0] [1] [2] ← 같은 메모리를 가리킴!데이터가 복사되지 않으므로:
- 메모리 절약: 1GB 배열의 슬라이스도 추가 메모리 불필요
- 빠름: 복사 시간이 0
- 수정 전파: 뷰를 수정하면 원본도 바뀜 (주의!)
뷰가 생기는 경우
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
# 1. 슬라이싱 → 뷰v1 = arr[2:5] # 뷰v1.base is arr # True (arr의 뷰임)
# 2. reshape → 뷰 (가능한 경우)v2 = arr.reshape(3, 3) # 뷰v2.base is arr # True
# 3. 전치 → 뷰mat = np.array([[1, 2], [3, 4]])v3 = mat.T # 뷰v3.base is mat # True
# 4. dtype 변환 (같은 크기) → 뷰v4 = arr.view(np.int64) # 뷰뷰인지 확인하는 방법
arr = np.array([1, 2, 3, 4, 5])sliced = arr[1:4]copied = arr[1:4].copy()
print(sliced.base is arr) # True — 뷰print(copied.base is None) # True — 독립 복사본 (base 없음)base가 None이면 자기 자신이 데이터 소유자(복사본), None이 아니면 다른 배열의 뷰입니다.
복사가 생기는 경우
arr = np.array([1, 2, 3, 4, 5])
# 1. 팬시 인덱싱 → 복사c1 = arr[[0, 2, 4]] # 복사!c1[0] = 99print(arr) # [1 2 3 4 5] — 원본 불변
# 2. 불리언 인덱싱 → 복사c2 = arr[arr > 3] # 복사!c2[0] = 99print(arr) # [1 2 3 4 5] — 원본 불변
# 3. 명시적 복사c3 = arr.copy() # 복사c3[0] = 99print(arr) # [1 2 3 4 5] — 원본 불변| 연산 | 결과 |
|---|---|
arr[2:5] (슬라이싱) | 뷰 |
arr[[0,2,4]] (팬시 인덱싱) | 복사 |
arr[arr > 3] (불리언 인덱싱) | 복사 |
arr.reshape(...) | 뷰 (가능하면) |
arr.copy() | 복사 |
arr.flatten() | 복사 |
arr.ravel() | 뷰 (가능하면) |
pandas와의 차이
import pandas as pd
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
# pandas에서 슬라이싱subset = df[df["A"] > 1]subset["B"] = 99# SettingWithCopyWarning 발생 가능!pandas는 뷰/복사 동작이 상황에 따라 달라져서 예측이 어렵습니다. 그래서 SettingWithCopyWarning이 존재합니다. 안전한 방법:
# 확실한 복사subset = df[df["A"] > 1].copy()subset["B"] = 99 # 원본 불변, 경고 없음
# 원본 수정이 목적이라면df.loc[df["A"] > 1, "B"] = 99 # 직접 수정실전에서 흔한 실수
함수 안에서 원본 수정
def normalize(data): # Bad: data가 뷰면 원본이 수정됨 data -= data.mean() return data
original = np.array([10.0, 20.0, 30.0])result = normalize(original[0:3]) # 슬라이싱 = 뷰!print(original) # [−10. 0. 10.] ← 원본이 변경됨!
# Good: 복사본에서 작업def normalize_safe(data): result = data.copy() result -= result.mean() return result대용량 데이터에서 뷰 활용
# 10GB 데이터의 일부만 분석huge_data = np.memmap("data.bin", dtype=np.float64, shape=(1_000_000_000,))
# 뷰: 추가 메모리 0chunk = huge_data[1000:2000] # 뷰 — 메모리 복사 없음
# 복사: 메모리 할당chunk_copy = huge_data[1000:2000].copy() # 복사 — 8KB 할당대용량 데이터에서는 뷰를 의도적으로 활용해서 메모리를 절약합니다. 수정이 필요한 경우에만 copy()를 호출합니다.
Python 리스트와의 비교
Python 리스트의 슬라이싱은 NumPy와 반대로 항상 복사를 반환합니다.
# Python 리스트: 슬라이싱 = 항상 얕은 복사py_list = [1, 2, 3, 4, 5]sliced = py_list[1:4]sliced[0] = 99print(py_list) # [1, 2, 3, 4, 5] — 원본 불변!
# NumPy: 슬라이싱 = 뷰 (공유)np_arr = np.array([1, 2, 3, 4, 5])sliced = np_arr[1:4]sliced[0] = 99print(np_arr) # [ 1 99 3 4 5] — 원본 변경!이 차이를 모르면 NumPy로 전환할 때 심각한 버그가 발생합니다. Python 리스트에서 안전하던 코드가 NumPy에서는 원본을 파괴할 수 있습니다.
얕은 복사 vs 깊은 복사
import copy
nested = [[1, 2], [3, 4]]
# 얕은 복사: 외부 리스트만 복사, 내부 리스트는 공유shallow = copy.copy(nested)shallow[0][0] = 99print(nested) # [[99, 2], [3, 4]] — 내부 리스트가 바뀜!
# 깊은 복사: 모든 중첩 객체까지 복사nested = [[1, 2], [3, 4]]deep = copy.deepcopy(nested)deep[0][0] = 99print(nested) # [[1, 2], [3, 4]] — 원본 불변NumPy의 .copy()는 데이터 전체를 복사하므로 깊은 복사와 동일한 효과입니다. NumPy 배열은 내부에 다른 Python 객체를 포함하지 않으므로(순수 숫자 데이터), 얕은/깊은 복사의 구분이 의미 없고, .copy() 하나로 충분합니다.
판단 플로차트
배열 연산 결과가 필요한가?
├── 원본을 수정해도 되는가?
│ ├── Yes → 뷰 사용 (슬라이싱 그대로)
│ └── No → .copy() 호출
└── 메모리가 충분한가?
├── Yes → .copy()로 안전하게
└── No → 뷰 사용, 수정 주의핵심 정리
| 개념 | 정리 |
|---|---|
| 뷰(View) | 같은 메모리를 공유. 수정하면 원본에 전파 |
| 복사(Copy) | 독립된 메모리. 수정해도 원본 불변 |
| 슬라이싱 | 뷰 반환 (NumPy) |
| 팬시/불리언 인덱싱 | 복사 반환 |
.base | None이면 복사본, 아니면 뷰 |
.copy() | 명시적 깊은 복사 |
NumPy의 뷰는 성능 최적화를 위한 설계입니다. 수 GB의 데이터를 복사 없이 다룰 수 있게 해주지만, "수정하면 원본도 바뀐다"는 부작용을 동반합니다. 규칙은 단순합니다: 슬라이싱 = 뷰, 팬시/불리언 = 복사. 확신이 없으면 .copy().
pandas 2.0부터 Copy-on-Write(CoW) 모드가 도입되어, 슬라이싱 결과를 수정할 때 자동으로 복사가 일어납니다. pd.options.mode.copy_on_write = True로 활성화할 수 있으며, 향후 기본 동작이 될 예정입니다. 하지만 NumPy는 여전히 명시적 뷰/복사 모델을 따르므로, 이 토픽의 규칙을 확실히 익혀야 합니다.