참고 자료
https://docs.scipy.org/doc/numpy/user/basics.rec.html
파이썬 버전 3.7 기준
NumPy 버전 1.16 기준
본 포스팅에서는 NumPy에서의 Structured Array를 다룬다.
NumPy에서의 Structured array
▷ numpy.genfromtxt()의 상세 내용을 다룬 포스팅에서 names 혹은 dtype를 정의하여 구성된 배열이 Structured array이다.
○ 아래 예제는 구조체 배열을 정의하는 것부터 배열의 각 요소를 어떻게 불러 올 수 있는지 보여주는 예이다.
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, datatype2, datatype3, ..."와 같다.
- 즉 자료형을 순서대로 입력하며 각 자료형은 콤마로 구분된다.
- 아이템사이즈와 필드의 바이트 오프셋은 자동으로 배정되며 필드의 이름은 기본값인 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과 바이트오프셋을 가지는 튜플로 구성된다.
○ 구조체가 아닌 배열에서는 names과 fields 어트리뷰트는 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 타입을 구조체 타입으로 해석하는 것이 가능하다.
○ 이 dtype은 C언어에서 union과 유사하다.
'Python > NumPy' 카테고리의 다른 글
Structured Array(구조체 배열)의 Record array(레코드 배열) - NumPy(11) (0) | 2019.02.25 |
---|---|
Structured Array(구조체 배열)의 indexing(인덱싱)과 assignment(배정) - NumPy(10) (0) | 2019.02.24 |
NumPy(넘파이)에서의 Byte-swapping(바이트스와핑) - NumPy(8) (0) | 2019.02.12 |
NumPy(넘파이)에서의 Broadcasting(브로드캐스팅) - NumPy(7) (0) | 2019.02.10 |
NumPy(넘파이)에서의 indexing(인덱싱) - NumPy(6) (0) | 2019.02.08 |
댓글