BioPlayground

🧬
목록으로

배열 뷰와 얕은 복사 — 수정 전파 주의

NumPy에서 슬라이싱이 뷰를 반환하는 이유, 수정 전파가 일어나는 조건, copy()로 방지하는 방법을 배웁니다.

중급
|
10
|
검증 완료 (2026-07)
배열 뷰얕은 복사깊은 복사수정 전파NumPy 메모리
진행률0/11 (0%)

배열 뷰와 얕은 복사 — 수정 전파 주의

이 토픽을 마치면

NumPy에서 뷰(view)와 복사(copy)의 차이를 이해하고, 슬라이싱이 뷰를 반환하는 이유를 설명할 수 있으며, 의도하지 않은 수정 전파를 방지할 수 있습니다.


놀라운 동작

python
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)란

뷰는 같은 데이터를 다른 관점으로 보는 것입니다.

text
메모리:  [1] [2] [3] [4] [5]
          ↑   ↑   ↑   ↑   ↑
original: [0] [1] [2] [3] [4]

sliced = original[1:4]
          ↑   ↑   ↑
sliced:  [0] [1] [2]   ← 같은 메모리를 가리킴!

데이터가 복사되지 않으므로:

  • 메모리 절약: 1GB 배열의 슬라이스도 추가 메모리 불필요
  • 빠름: 복사 시간이 0
  • 수정 전파: 뷰를 수정하면 원본도 바뀜 (주의!)

뷰가 생기는 경우

python
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) # 뷰

뷰인지 확인하는 방법

python
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 없음)

baseNone이면 자기 자신이 데이터 소유자(복사본), None이 아니면 다른 배열의 뷰입니다.


복사가 생기는 경우

python
arr = np.array([1, 2, 3, 4, 5])
# 1. 팬시 인덱싱 → 복사
c1 = arr[[0, 2, 4]] # 복사!
c1[0] = 99
print(arr) # [1 2 3 4 5] — 원본 불변
# 2. 불리언 인덱싱 → 복사
c2 = arr[arr > 3] # 복사!
c2[0] = 99
print(arr) # [1 2 3 4 5] — 원본 불변
# 3. 명시적 복사
c3 = arr.copy() # 복사
c3[0] = 99
print(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와의 차이

python
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이 존재합니다. 안전한 방법:

python
# 확실한 복사
subset = df[df["A"] > 1].copy()
subset["B"] = 99 # 원본 불변, 경고 없음
# 원본 수정이 목적이라면
df.loc[df["A"] > 1, "B"] = 99 # 직접 수정

실전에서 흔한 실수

함수 안에서 원본 수정

python
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

대용량 데이터에서 뷰 활용

python
# 10GB 데이터의 일부만 분석
huge_data = np.memmap("data.bin", dtype=np.float64, shape=(1_000_000_000,))
# 뷰: 추가 메모리 0
chunk = huge_data[1000:2000] # 뷰 — 메모리 복사 없음
# 복사: 메모리 할당
chunk_copy = huge_data[1000:2000].copy() # 복사 — 8KB 할당

대용량 데이터에서는 뷰를 의도적으로 활용해서 메모리를 절약합니다. 수정이 필요한 경우에만 copy()를 호출합니다.


Python 리스트와의 비교

Python 리스트의 슬라이싱은 NumPy와 반대로 항상 복사를 반환합니다.

python
# Python 리스트: 슬라이싱 = 항상 얕은 복사
py_list = [1, 2, 3, 4, 5]
sliced = py_list[1:4]
sliced[0] = 99
print(py_list) # [1, 2, 3, 4, 5] — 원본 불변!
# NumPy: 슬라이싱 = 뷰 (공유)
np_arr = np.array([1, 2, 3, 4, 5])
sliced = np_arr[1:4]
sliced[0] = 99
print(np_arr) # [ 1 99 3 4 5] — 원본 변경!

이 차이를 모르면 NumPy로 전환할 때 심각한 버그가 발생합니다. Python 리스트에서 안전하던 코드가 NumPy에서는 원본을 파괴할 수 있습니다.

얕은 복사 vs 깊은 복사

python
import copy
nested = [[1, 2], [3, 4]]
# 얕은 복사: 외부 리스트만 복사, 내부 리스트는 공유
shallow = copy.copy(nested)
shallow[0][0] = 99
print(nested) # [[99, 2], [3, 4]] — 내부 리스트가 바뀜!
# 깊은 복사: 모든 중첩 객체까지 복사
nested = [[1, 2], [3, 4]]
deep = copy.deepcopy(nested)
deep[0][0] = 99
print(nested) # [[1, 2], [3, 4]] — 원본 불변

NumPy의 .copy()는 데이터 전체를 복사하므로 깊은 복사와 동일한 효과입니다. NumPy 배열은 내부에 다른 Python 객체를 포함하지 않으므로(순수 숫자 데이터), 얕은/깊은 복사의 구분이 의미 없고, .copy() 하나로 충분합니다.


판단 플로차트

text
배열 연산 결과가 필요한가?
├── 원본을 수정해도 되는가?
│   ├── Yes → 뷰 사용 (슬라이싱 그대로)
│   └── No  → .copy() 호출
└── 메모리가 충분한가?
    ├── Yes → .copy()로 안전하게
    └── No  → 뷰 사용, 수정 주의

핵심 정리

개념정리
뷰(View)같은 메모리를 공유. 수정하면 원본에 전파
복사(Copy)독립된 메모리. 수정해도 원본 불변
슬라이싱뷰 반환 (NumPy)
팬시/불리언 인덱싱복사 반환
.baseNone이면 복사본, 아니면 뷰
.copy()명시적 깊은 복사

NumPy의 뷰는 성능 최적화를 위한 설계입니다. 수 GB의 데이터를 복사 없이 다룰 수 있게 해주지만, "수정하면 원본도 바뀐다"는 부작용을 동반합니다. 규칙은 단순합니다: 슬라이싱 = 뷰, 팬시/불리언 = 복사. 확신이 없으면 .copy().

pandas 2.0부터 Copy-on-Write(CoW) 모드가 도입되어, 슬라이싱 결과를 수정할 때 자동으로 복사가 일어납니다. pd.options.mode.copy_on_write = True로 활성화할 수 있으며, 향후 기본 동작이 될 예정입니다. 하지만 NumPy는 여전히 명시적 뷰/복사 모델을 따르므로, 이 토픽의 규칙을 확실히 익혀야 합니다.