본문 바로가기
Python/NumPy

Structured Array(구조체 배열)의 개요와 생성 - NumPy(9)

by 콩돌 2019. 2. 23.
반응형

참고 자료

https://docs.scipy.org/doc/numpy/user/basics.rec.html



파이썬 버전 3.7 기준

NumPy 버전 1.16 기준


본 포스팅에서는 NumPy에서의 Structured Array를 다룬다.



NumPy에서의 Structured array


Structured Array(구조체 배열) 개요

구조체 배열 개요
  ○ Structured Array(구조체 배열)는 ndarray이며, Field라고 명명된 시퀀스로써 구성되어있는 간단한 데이터 타입으로 구성되어져있다.

    ▷ numpy.genfromtxt()의 상세 내용을 다룬 포스팅에서 names 혹은 dtype를 정의하여 구성된 배열이 Structured array이다.

  ○ 아래 예제는 구조체 배열을 정의하는 것부터 배열의 각 요소를 어떻게 불러 올 수 있는지 보여주는 예이다.


구조체 배열의 예제)
# 구조체 배열 입력
In[3]: y = np.array([('Ramen', 5000.0, 'KRW'), ('GimBab',2000.0,'KRW'), ('Pasta', 7.0, 'USD')], dtype=[('Menu', 'U10'), ('Price','f4'), ('Unit', 'U10')])

# 구조체 배열 구성
In[4]: y
Out[4]: 
array([('Ramen', 5000., 'KRW'), ('GimBab', 2000., 'KRW'), ('Pasta',    7., 'USD')],
      dtype=[('Menu', '<U10'), ('Price', '<f4'), ('Unit', '<U10')])

# 구조체 배열 인덱싱 1
In[5]: y[0]
Out[5]: ('Ramen', 5000., 'KRW')

# 구조체 배열 인덱싱 2
In[6]: y['Menu']
Out[6]: array(['Ramen', 'GimBab', 'Pasta'], dtype='<U10')

# 구조체 배열의 배정
In[7]: y['Price'][2] = 15.5
In[8]: y['Price']
Out[8]: array([5000. , 2000. ,   15.5], dtype=float32)


Structured datatype(구조체 자료형) 개요

  ○ Structured datatype(구조체 자료형)은 C언어에서 structs와 유사하게 설계되며, 유사한 메모리 레이아웃을 공유한다.

  ○ 구조체 자료형은 C 코드와의 인터페이스 및 구조체 버퍼의 저수준 조작(예 : 바이너리 블롭 해석)에 사용된다.

  ○ 이런 목적으로, Sub-array(서브 배열), 중첩된 데이터 유형 및 공용체와 같은 특수 기능을 지원하고 구조의 메모리 레이아웃을 제어 할 수 있게 한다.


구조체 자료를 다룰때 더 유용한 도구

  ○ 사실 사용자는 csv파일과 같이 표데이터를 조작할 때, xarray나 pandas, DataArray와 같은 다른 파이썬 라이브러리를 찾는다.

    ▷ 위와 같은 툴은 표데이터를 다루는데 있어 더 높은 수준의 인터페이스를 제공하며, 더욱 최적화되어 있다. 

     예를들어, 넘파이에서 구조체배열의 C 구조체 같은 메모리 레이아웃은 빈약한 캐시 동작을 초래할 수도 있다. 



Structured Datatype(구조체 자료형)


구조체 자료형은 field의 집합처럼 해석될 수 있는 명확한 길이의 바이트의 시퀀스로써 여겨질 수 있다.


각 필드는 구조체 자료형 내부에 name(이름), datatype(자료형), byte offset(바이트 오프셋)을 가진다. 


필드의 자료형은 다른 구조체 자료형을 포함해서 어떤 넘파이의 자료형이 될 수 있다.

필드의 자료형은  또한 명확한 형태의 ndarray와 같이 작동하는 서브 배열이 될 수 도 있다.


필드의 오프셋은 임의적이며, 겹치는 것(overlap)도 가능하다. 

이러한 오프셋은 일반적으로 자동적으로 넘파이에 의해 결정되어지나, 또한 지정하는 것 역시 가능하다.



구조체 자료형 생성

  ○ 구조체 자료형은 numpy.dtype를 사용하여 만들어질 수 있다. 

  ○ 구조체 자료형을 작성하는 방법으로 크게 4가지 방법이 있으며, 이는 유연하며, 간결하게 사용되어질 수 있다. 

  ○ 이 부분에 대한 매우 자세한 내용은 여기에 설명되어 있으며, 아래에는 간략한 설명을 남겨놓는다.

 

    ① 튜플의 리스트, 필드당 하나의 튜플을 활용하는 방법

      - 각 튜플은 (fieldname, datatype, shape) 형태로 구성되며 shape은 선택적으로 사용될 수 있다.

        ☞ fieldname: 필드의 이름을 지정하는 문자열이다. (비어있을 경우('') 기본 이름 값이 들어가며 문자열 f와 숫자의 조합으로 입력된다.)

         datatype: 필드의 전환 가능한 자료형 오브젝트이다.

         shape: 정수로 입력되며, 서브배열의 형상을 나타낸다.

      - 구조체와 모든 구조체의 아이템사이즈 이내의 필드의 바이트 오프셋은 자동으로 지정된다. 


    ② dtype을 명시하는 방법으로 콤마로 분리된 문자열을 활용하는 방법

      - 입력 형태는 "datatype1, datatype2datatype3, ..."와 같다.

      - 즉 자료형을 순서대로 입력하며 각 자료형은 콤마로 구분된다.

      - 아이템사이즈와 필드의 바이트 오프셋은 자동으로 배정되며 필드의 이름은 기본값인 f0, f1, f2, ... 형태로 입력된다.


    ③ 필드 파라미터 배열의 딕셔너리를 활용하는 방법

      - {'names': ['name1', 'name2', ...], 'formats': ['datatype1', 'datatype2', ...]} 형태로 입력될 수 있다.

        ☞ 딕셔너리의 key에는 제어하고자 하는 카테고리를 입력하며, value에는 각 카테고리에 해당하는 값을 입력한다. 각 카테고리(lkey)에 대한 설명은 다음과 같다.

        ☞ names: 필드의 이름을 입력하며, 리스트로 입력되어지고 formats와 같은 길이를 가진다. (필수입력)

        ☞ formats: 자료형이 입력되며, 리스트로 입력되어지고 names와 같은 길이를 가진다. (필수입력)

        ☞ offsets: 바이트오프셋을 입력하기 위한 정수의 리스트로 구성되어진다. (선택적 입력)

        ☞ itemsize: dtype의 바이트내에서 총 크기를 정의하는 정수로 입력되며, 모든 필드를 담을 수 있는 충분한 크기여야 한다. (선택적 입력)

         aligned: 값이 True일 경우, numpy.dtype의 'align'키워드 인수가 True로 설정된 것처럼 자동 오프셋 계산이 정렬된 오프셋을 사용하도록 만든다. (선택적 입력)

        ☞ titles: names와 같은 길이를 가지는 리스트가 입력되며 타이틀이 입력되어진다. (선택적 입력)


      - 이 방법으로는 필드의 바이트 오프셋과 구조의 아이템사이즈를 제어할 수 있기 때문에 상당히 유연하게 사용되어질 수 있다.

      - 필드의 데이터를 손상시킬 수 있긴 하지만, 오프셋은 겹치도록 선택되어질 수 있다. 예외적으로, 내부 오브젝트 포인터의 손상이나 역참조의 리스크 때문에 역참조 numpy.object 타입은 겹처질 수 없다.


    ④ 필드 이름의 딕셔너리를 활용하는 방법

      - {'name1': ('datatype1', offset), 'name2': ('datatype2', offset), ... }

        ☞ name 자리에는 사용자가 사용하고자 하는 필드 이름이 들어간다. 

        ☞ datatype에는 필드의 자료형이 입력된다.

        ☞ offset에는 필드의 오프셋이 입력된다.

        ☞ 각 key에 해당되는 value는 튜플 형식으로 입력된다.

      - 이 방법은 도태된 형태의 입력방식이며, 오래된 numpy 코드에서 사용되었던 방식이다.


     이 방법들은 genfromtxt의 name 사용하는 방법과 상당히 유사하다.


각 방식의 예)

# ①번 방법

In[12]: np.dtype([('X01', 'f4'), ('X02', np.float32), ('X03', 'i4', (5,5))])

Out[12]: dtype([('X01', '<f4'), ('X02', '<f4'), ('X03', '<i4', (5, 5))])


# ②번 방법

In[13]: np.dtype('i4, i2, f4, U10')

Out[13]: dtype([('f0', '<i4'), ('f1', '<i2'), ('f2', '<f4'), ('f3', '<U10')])


# ③번 방법

In[14]: np.dtype({'names': ['X01', 'X02', 'X03'], 'formats': ['i4', 'i2', 'f4']})

Out[14]: dtype([('X01', '<i4'), ('X02', '<i2'), ('X03', '<f4')])


# ④번 방법

In[15]: np.dtype({'X01': ('i4', 0), 'X02': ('f4', 5)})

Out[15]: dtype({'names':['X01','X02'], 'formats':['<i4','<f4'], 'offsets':[0,5], 'itemsize':9})


구조체 자료형의 조작과 화면출력

  ○ 구조체 자료형의 필드 이름으로 구성된 리스트는 dtype 오브젝트의 numpy.dtype.names 어트리뷰트에서 찾아볼 수 있다.

  ○ 같은 길이의 문자열의 시퀀스를 사용해서 numpy.dtype.names 어트리뷰트를 수정함으로써 필드 이름이 수정될 수 있다. 

  ○ numpy.dtype 오브젝트는 딕셔너리와 같은 어트리뷰트인 fields를 가지며, key는 필드의 이름이고 value는 각 필드의 dtype과 바이트오프셋을 가지는 튜플로 구성된다.

  ○ 구조체가 아닌 배열에서는 namesfields 어트리뷰트는 None값을 가진다. 

    ▷ 구조체 배열인지 판단하기 위해 권장되는 방법은 "if dt.names is not None"를 사용하는 것이 "if dt.names" 보다 더 좋다.

  ○ 구조체 자료형의 문자열 표현은 만약 가능하다면 튜플의 리스트 형태로 출력되며, 그렇지 않다면 일반적인 딕셔너리 형식으로 표현된다.


예제)

# 배열 입력

In[21]: y = np.array([('Ramen', 5000.0, 'KRW'), ('GimBab',2000.0,'KRW'), ('Pasta', 7.0, 'USD')], dtype=[('Menu', 'U10'), ('Price','f4'), ('Unit', 'U10')])


# names 메서드를 통한 필드 이름 출력

In[22]: y.dtype.names

Out[22]: ('Menu', 'Price', 'Unit')


# fields 메서드를 통한 필드 출력

In[23]: y.dtype.fields

Out[23]: 

mappingproxy({'Menu': (dtype('<U10'), 0),

              'Price': (dtype('float32'), 40),

              'Unit': (dtype('<U10'), 44)})


자동 바이트오프셋과 정렬

  ○ 넘파이는 자동적으로 필드의 바이트 오프셋과 구조체 자료형의 전체적인 아이템사이즈를 결정하기 위해 두 가지 방법중 하나를 선택해서 사용한다. 

    ▷ align=False 일 경우 : 

      - align은 False가 기본 값이다.

      - 이 경우, 넘파이에서는 필드들을 한대  묶는데 각각의 필드는 이전 필드의 바이트오프셋의 끝나는 위치부터 시작한다.

      - 필드들은 메모리상에서 연속적으로 연결되어 있다.

    ▷ align=True 일 경우 : 

      - 이 경우, 넘파이에서는 많은 C 컴파일러가 C 구조체를 채우는 것과 같은 방식으로 구조체를 채우게된다. 

      - 자료형의 사이즈가 커져있는 것과 같은 상황에서 정렬된 구조체는 성능개선을 기대할 수 있다.

      - 필드 사이에 패딩 바이트(Padding byte)가 삽입되며, 각각의 필드의 바이트오프셋은 필드의 일직선의 배수가 된다.

      - 일반적으로 이 일직선은 단순한 자료형에 대해서는 바이트상의 필드의 크기와 동일하다.

      - PyArray_Descr.alignment에 관련된 자세한 내용이 존재한다.

      - 구조체에는 후행 패딩이 추가되어 아이템사이즈가 가장 큰 필드의 정렬의 배수가 되도록 한다.


  ○ 만약 offsets 변수를 입력되어 오프셋을 따로 입력하였다면, align=True로 셋팅하는 것은 각각의 필드에 오피셋이 그 크기의 배수인지 확인해야하며, 아이템사이즈 역시 가장큰 필드 사이즈의 배수인지 확인해야할 것이다. 그렇지 않은 경우 예외가 발생할 수 있다.

    ▷ 만약 필드의 오프셋과 구조체배열의 아이템 사이즈가 정렬 조건을 만족한다면 그 배열은 ALIGNED 플래그가 셋팅된다. 

  ○ numpy.lib.recfunctions.repack_fields라는 함수는 정렬된 dtype이나 배열을 압축 시키거나 혹은 그 반대로 만들 수 있다.

    ▷ 이것은 독립변수로써 dtype이나 구조체 ndarray를 가지게 되고, 패딩바이트를 사용하거나 혹은 사용하지 않은 다시 채운 필드의 복사본를 반환한다.


※ 참고사항 

최근 대부분의 C 컴파일러가 이 방법을 기본으로하여 패딩을 삽입하지만, C 구조체에서의 패딩은 C 구현에 의존적이므로, 메모리 레이아웃은 C 프로그램상의 해당 구조체와 정확하게 매칭되는 것이 보장되지 않는다. 넘파이 측과 C 측간을 정확히 일치시키기 위해서 몇가지 작업이 수행되어져야 할 것이다. (솔직히 컴전공이 아니라 뭔말인지 모르겠다..)


예제)

# 사용할 함수 정의 

In[30]: def offset(d):

     ...:    print('offset: ', [d.fields[xx][1] for xx in d.names])

     ...:    print('itemsize: ', d.itemsize)

    

# align=False인 경우 

In[31]: offset(np.dtype('i2,i4,i8,u2,u4,u8'))

offset:  [0, 2, 6, 14, 16, 20]

itemsize:  28


# align=True인 경우 

In[32]: offset(np.dtype('i2,i4,i8,u2,u4,u8',align=True))

offset:  [0, 4, 8, 16, 20, 24]

itemsize:  32


필드 타이틀

  ○ 필드 이름 외에 필드에는 필드에 대한 추가 설명 또는 별칭으로 사용되는 타이틀이 있을 수 있다.

    ▷ 타이틀은 이름과 마찬가지로 배열에 인덱싱을 하는데 사용될 수 있다.

  ○ 위의 ① 방법(튜플의 리스트 활용)에서 이용할 경우 타이틀을 추가하기 위해 필드 이름은 하나의 문자열 대신에 두개의 문자열을 가지는 튜플로써 명시될 수 있다.

    ▷ [(('title1', 'name1'), datatype1), (('title2', 'name2'), datatype2), ... ] 형태로 입력된다.

    ▷ 각 튜플 요소는 필드의 타이틀과 이름을 의미한다.

  ○ 위의 ④ 방법(딕셔너리 활용)에서 필드 타이틀을 입력할 시에는 각 키에 존재하는 튜플에서 마지막 요소를 타이틀로 입력하면 된다.

    ▷ {'name': (datatype, offset, 'title')} 형태로 입력된다.

  ○ 타이틀이 사용될 경우 dtype.fields 딕셔너리는 키로써 타이틀을 포함한다.

    ▷ 이것은 타이틀을 사용한 필드는 필드 딕셔너리에 두번 표시되는 것을 의미한다.

    ▷ 이런 필드에는 튜플의 값은 필드 타이틀이라는 3번째 요소를 가진다. 

      - fields 어트리뷰트가 순서를 보존하지 않는 반면 names 어트리뷰트가 필드 순서를 보존한다.

      - 위의 이유들로 for문과 같은 반복문을 사용할 때, 타이틀을 나열하지 않는 dtype의 names 어트리뷰트를 사용하여 dtype 필드를 하는 것이 권장된다. 


예제)

# ①의 방법 이용

In[36]: np.dtype([(('Title1', 'name1'), 'f4'), (('Title2', 'name2'), 'f4')])

Out[36]: dtype([(('Title1', 'name1'), '<f4'), (('Title2', 'name2'), '<f4')])


# ④의 방법 이용

In[37]: np.dtype({'name1':('f4',0,'title1'), 'name2':('f4',5,'title2')})

Out[37]: dtype({'names':['name1','name2'], 'formats':['<f4','<f4'], 'offsets':[0,5], 'titles':['title1','title2'], 'itemsize':9})



유니온 타입(Union type)

  ○ 넘파이에서 구조체 자료형은 기본값으로써 베이스 타입인 numpy.void을 가지도록 구현된다. 

    ▷ (base_dtype, dtype) 형식을 사용하여 다른 numpy 타입을 구조체 타입으로 해석하는 것이 가능하다.

  ○ base_dtype은 기본 dtype이며 필드와 플래그는 dtype으로 부터 복사된다.

  ○ 이 dtype은 C언어에서 union과 유사하다.



반응형

댓글