4.5.2 복사(copy)와 보기(뷰, view)

① ndarray의 데이터와 메타데이터

NumPy 배열에서 작업할 때 데이터를 복사(copy)하지 않고 보기(view)를 사용하여 직접 내부 데이터에 접근할 수 있다. 보기를 사용하면 좋은 성능을 보장하지만, 사용자가 원하지 않는 문제를 일으킬 수도 있다. 그러므로 차이점을 아는 것이 중요하다. NumPy의 함수 중에는 복사본을 반환하는 함수도 있고 보기(view)를 반환하는 함수도 있으니 주의해 사용해야 한다.

NumPy 배열은 두 부분으로 구성된 데이터 구조이다. 하나는 실제 데이터 요소가 저장된 연속된 데이터 저장소이며, 다른 하나는 데이터 저장소에 대한 정보를 갖는 메타데이터(metadata)이다. 메타데이터는 데이터 유형, 보폭(stride) 및 ndarray를 쉽게 조작할 수 있도록 도움이 되는 기타 중요한 정보를 말한다.

② 보기(view)와 얕은 복사

[비유로 이해하기: 1개의 거대한 본체와 여러 개의 홀로그램(View)] 천만 개의 데이터가 들어있는 거대한 엑셀 파일(메모리 원본)이 하나 있습니다. 이 원본을 수정하기 위해 엑셀 파일을 통째로 복사본(Ctrl+C, Ctrl+V)을 만들면 컴퓨터 용량이 낭비됩니다. 대신 Numpy는 데이터 자체는 원본 그대로 하나만 놔두고, 바라보는 렌즈(홀로그램 카메라)의 각도나 해상도만 바꿔서 마치 새로운 배열처럼 보여주는 기술을 극대화하여 사용합니다. 이것이 바로 보기(뷰, View)입니다.

데이터 저장소는 동일시 유지되므로 홀로그램(View)에 가해진 타격(수정)은 원본 데이터에도 100% 동일하게 반영됩니다. reshape()이나 타겟팅 범위를 잘라내는 슬라이싱(a[1:4])은 대부분 메모리를 낭비하지 않는 이 ‘뷰(View)’를 반환합니다.

다음 변수 org에는 모양 (5, )의 1차원 배열이 저장된다.

org = np.arange(5)
org

출력:

array([0, 1, 2, 3, 4])

함수 ndarray.view()org와 동일한 데이터를 갖는 보기(view)가 표시될 수 있다.

v = org.view()
v

출력:

array([0, 1, 2, 3, 4])

다음 코드로 뷰를 수정하면 원본 org에도 반영이 된다. 보기는 이러한 수정에 주의해야 한다.

v[2] = 200
v

출력:

array([  0,   1, 200,   3,   4])
org

출력:

array([  0,   1, 200,   3,   4])

③ 복사(copy)

데이터 버퍼와 메타 데이터를 복제하여 새로운 배열을 생성할 때 복사(copy)라고 한다. 복사본에 대한 변경 사항은 원래 배열에 반영되지 않는다. 복사는 속도도 느리고 새로운 메모리가 필요하지만 필요할 때가 있다.

다음 변수 org에는 모양 (5, )의 1차원 배열이 저장된다.

org = np.arange(5)
org

출력:

array([0, 1, 2, 3, 4])

다음 org.copy()org와 다른 복사된 새로운 배열 c가 생성된다.

c = org.copy()
c

출력:

array([0, 1, 2, 3, 4])

다음 코드로 배열 c를 수정한다.

c[2] = 200
c

출력:

array([  0,   1, 200,   3,   4])

위 수정이 원본 org에는 전혀 반영되지 않는다. 배열 orgc는 다른 배열이다.

org

출력:

array([0, 1, 2, 3, 4])

④ 배열이 뷰인지 복사본인지 확인하는 방법

ndarraybase 속성을 사용하면 쉽게 배열이 뷰인지 복사본인지 알 수 있다. 뷰의 base 속성은 원본 배열을 반환한다. 반면, 복사본의 base 속성은 None을 반환한다.

다음 모양 (6, )의 1차원 배열 x가 있다.

x = np.arange(6)
x

출력:

array([0, 1, 2, 3, 4, 5])

함수 x.reshape(2, 3)로 모양이 수정된 보기를 반환해 변수 y에 저장한다.

y = x.reshape(2, 3) # view를 반환
y

출력:

array([[0, 1, 2],
       [3, 4, 5]])

y.base의 결과는 원본 배열 x가 표시된다.

y.base

출력:

array([0, 1, 2, 3, 4, 5])

다음 z는 전치 행렬의 결과를 저장한 y의 뷰인 것을 알 수 있다.

z = y.transpose()
z.base

출력:

array([0, 1, 2, 3, 4, 5])

z.copy()로 새로운 배열을 저장한 변수 c의 속성 baseNone이다.

c = z.copy()
print(c.base)

출력:

None

⑤ ndarray의 보기로 같은 구조만 수정 가능

다음 파이썬의 리스트는 8개의 항목으로 구성된다.

lst = list(range(8))
lst

출력:

[0, 1, 2, 3, 4, 5, 6, 7]

첨자 3번과 4번의 부분 리스트를 3개의 원소가 있는 리스트로 수정할 수 있다.

lst[3:5] = [30, 40, 50]
lst

출력:

[0, 1, 2, 30, 40, 50, 5, 6, 7]

다음은 모양 (8, )의 1차원 ndarrayx에 저장한다.

x = np.arange(8)
x

출력:

array([0, 1, 2, 3, 4, 5, 6, 7])

다음으로 첨자 3번과 4번의 원소를 각각 30, 40으로 수정한다.

x[3:5] = [30, 40]
x

출력:

array([ 0,  1,  2, 30, 40,  5,  6,  7])

그런데, 파이썬 목록과는 다르게 첨자 3번과 4번의 원소 개수가 다른 30, 40, 50으로 수정이 불가능하다. 원본과 같은 개수의 값만 수정이 가능하다.

x[3:5] = [30, 40, 50]

오류:

ValueError: could not broadcast input array from shape (3,) into shape (2,)

⑥ 얕은 복사(shallow copy)

ndarray의 일반 대입은 단지 얕은 복사로서 변수 이름이 같은 배열을 참조한다. 그러므로 is 연산 결과가 True이다.

a = np.array([[1, 2, 3], [4, 5, 6]])
b = a
b is a

출력:

True

다음처럼 함수 id()의 결과가 같다.

print(id(a))
print(id(b))

배열 ba와 같으므로 b.baseNone을 반환한다.

print(b.base)

출력:

None

배열 ba와 같으므로 b[0, 0] = 100으로 수정해도 a가 수정된다. 마치 보기와 비슷하다.

b[0, 0] = 100
a

출력:

array([[100,   2,   3],
       [  4,   5,   6]])

a.reshape(3, 2)로는 보기가 만들어진다.

v = a.reshape(3, 2)
v

출력:

array([[100,   2],
       [  3,   4],
       [  5,   6]])

보기는 얕은 복사와 달리 다음처럼 메모리 주소가 다르다.

print(id(a))
print(id(v))

보기 vv[0, 1] = 200을 수정하면 원본 a가 수정된다.

v[0, 1] = 200
a

출력:

array([[100, 200,   3],
       [  4,   5,   6]])

v는 보기이므로 기반(a)이 표시된다.

v.base

출력:

array([[100, 200,   3],
       [  4,   5,   6]])
서브목차