배열 이해 - 1

이준우·2024년 9월 19일

공부를 하면서 기본기가 매우 부족하다는 것을 느낀다.

인공지능을 공부하면서 numpy를 자주 사용하는데, 최근들어 자료구조 개념이 매우 부족하다는 것을 느껴 작성을 해본다.

코드 하나를 보자.

import numpy as np

a = np.array([[1], [2], [3]]

print(id(a[0]))  # a[0] = [1]
print(id(a[1]))  # a[1] = [2]
print(id(a[2]))  # a[2] = [3]

이런 코드를 눈앞에서 마주쳤을때, 출력은 어떨지 생각해보라. 각 주소값은 동일할까? 다를까?
numpy도 배열이다. 배열이라면 운영체제로부터 메모리가 연속된 블럭을 할당받고 거기에 데이터를 저장한다.

출력을 확인해보자.

print(id(a[0])) : 137328779714960
print(id(a[0])) : 137328779714960
print(id(a[0])) : 137328779714960

이상하다. 분명히 값은 다른데, 모두 같은 주소를 반환하고 있다. 파이썬의 id()는 객체의 주소를 반환하기 때문에 이런 것일까? 반은 맞고 반은 틀리다.

위의 코드는 배열 참조에 대한 주소값이기 때문이다. a라는 배열의 참조값을 가리키고 있기 때문에, 해당 코드는 모두 같은 주소값을 반환한다. 여기서 파이썬의 view개념이 등장한다.

view란 무엇인가?

원본 데이터의 일부 혹은 전체를 복사하지 않고 참조하는 방식이다. 해당 방식은 메모리 절약과 빠른 연산이 장점이지만 복사하지 않기 때문에 원본을 수정하는 리스크가 존재한다.

그래... 좋아! 그럼 다음과 같은 코드를 작성해보자.

print(id(a[0][0]))   # a[0][0] = 1
print(id(a[1][0]))   # a[1][0] = 2
print(id(a[2][0]))   # a[2][0] = 3

해당 코드의 출력은 어떨까?

print(id(a[0][0])) : 137328781562640
print(id(a[1][0])) : 137328781567856
print(id(a[2][0])) : 137328781570832

이것도 뭔가 이상하다. 주소를 나타내고 있긴한데, 메모리의 주소가 8씩 늘어나지 않고 비연속적으로 메모리가 늘어나는 것을 확인할 수 있다. 위에서 말한 것처럼 배열은 연속된 메모리를 갖고 있어야 한다. 운영체제가 연속된 메모리를 가진 블럭을 배열에 할당해주기 때문이다. 근데, 파이썬의 id()함수로 출력해서 보면 그렇지 않은 것을 확인할 수 있다.

이 이유는 id()함수는 데이터가 저장된 주소를 반환하는 것이 아니라 객체의 메모리 주소를 반환한다. 객체 안에는 데이터 타입과 참조 횟수등을 갖고 있는 메타데이터를 포함하므로 이러한 객체 주소를 반환하기 때문에 우리가 원하는 데이터가 저장된 주소값을 뽑아내기에는 문제가 있다.

이런 문제를 해결하려면 numpy에서 제공하는 ctypes.data와 strides로 해결이 가능하다. 예제로 알아보자.

import numpy as np

a = np.array([[1], [2], [3]])

address = a.ctypes.data

for i in range(a.shape[0]):
	a_data_address = address + i * a.strides(0)
	print("a_data_address{idx} : {addr}".format(idx=i, addr=hex(a_data_address)) 

이처럼 구현하면 numpy안의 데이터가 저장되어 있는 주소에 접근할 수 있게 되며, 우리가 원하는 연속된 메모리 주소를 확인할 수 있다. (8씩 증가하는 메모리 구조를 볼 수 있게된다.)

profile
멋진 인생을 살기 위한 footprint

0개의 댓글