struct — 바이트를 패킹된 이진 데이터로 해석하기
struct — 바이트를 패킹된 이진 데이터로 해석하기
이 모듈은 Python 값과, Python bytes 객체로 표현된 C 구조체 사이를 변환해요. 컴팩트한 포맷 문자열이 Python 값으로의/로부터의 의도된 변환을 설명하죠. 이 모듈의 함수와 객체는 크게 구분되는 두 응용에 쓰일 수 있어요. 하나는 외부 소스(파일이나 네트워크 연결)와의 데이터 교환이고, 다른 하나는 Python 애플리케이션과 C 레이어 사이의 데이터 전송이에요.
참고 — 접두 문자(prefix character)가 없으면 기본값은 네이티브 모드예요. Python 인터프리터가 빌드된 플랫폼과 컴파일러를 기준으로 데이터를 패킹하거나 언패킹하죠. 주어진 C 구조체를 패킹한 결과에는 관련 C 타입들의 올바른 정렬을 유지하기 위한 패드 바이트가 포함돼요. 마찬가지로 언패킹할 때도 정렬이 고려돼요. 반대로 외부 소스와 데이터를 주고받을 때는 요소들 사이의 바이트 순서와 패딩을 정의하는 책임이 프로그래머에게 있어요. 자세한 내용은 Byte Order, Size, Alignment를 보세요.
여러 struct 함수(와 Struct 메서드)는 buffer 인자를 받아요. 이것은 Buffer Protocol을 구현하고 읽기 가능하거나 읽기-쓰기 가능한 버퍼를 제공하는 객체를 가리켜요. 그 목적으로 가장 흔히 쓰이는 타입은 bytes와 bytearray지만, 바이트 배열로 볼 수 있는 많은 다른 타입도 버퍼 프로토콜을 구현해서 bytes 객체에서 추가 복사 없이 읽거나 채울 수 있어요.
출처: Python 표준 라이브러리
본문
함수와 예외
모듈은 다음 예외와 함수들을 정의해요.
exception struct.error — 다양한 상황에서 발생하는 예외. 인자는 무엇이 잘못되었는지 설명하는 문자열이에요.
struct.pack(format, v1, v2, ...)— 포맷 문자열format에 따라 패킹된 값 v1, v2, …을 담은 bytes 객체를 반환해요. 인자는 포맷이 요구하는 값과 정확히 일치해야 해요.struct.pack_into(format, buffer, offset, v1, v2, ...)— 포맷 문자열format에 따라 값 v1, v2, …을 패킹하고 패킹된 바이트를 위치offset부터 시작하는 쓰기 가능한 버퍼buffer에 써요.offset은 필수 인자임을 주의하세요. 음수offset은buffer의 끝에서부터 센답니다.struct.unpack(format, buffer)— 포맷 문자열format에 따라 (아마pack(format, ...)으로 패킹된) 버퍼buffer에서 언패킹해요. 정확히 하나의 항목만 포함해도 결과는 튜플이에요. 버퍼의 바이트 크기는calcsize()가 반영하는 포맷이 요구하는 크기와 일치해야 해요.struct.unpack_from(format, /, buffer, offset=0)— 포맷 문자열format에 따라 위치offset부터 시작하는buffer에서 언패킹해요. 정확히 하나의 항목만 포함해도 결과는 튜플이에요. 위치offset에서 시작하는 버퍼의 바이트 크기는calcsize()가 반영하는 포맷이 요구하는 크기 이상이어야 해요. 음수offset은buffer의 끝에서부터 세요.struct.iter_unpack(format, buffer)— 포맷 문자열format에 따라 버퍼buffer에서 반복적으로 언패킹해요. 이 함수는 내용이 모두 소비될 때까지 버퍼에서 같은 크기의 청크를 읽는 이터레이터를 반환해요. 버퍼의 바이트 크기는calcsize()가 반영하는 포맷이 요구하는 크기의 배수여야 해요. 각 반복은 포맷 문자열이 지정하는 튜플을 만들어내요. (버전 3.4에 추가됨.)struct.calcsize(format)— 포맷 문자열format에 대응하는 구조체(pack(format, ...)이 만들어내는 bytes 객체)의 크기를 반환해요.
포맷 문자열
포맷 문자열은 데이터를 패킹하고 언패킹할 때 데이터 레이아웃을 설명해요. 패킹/언패킹되는 데이터의 타입을 지정하는 포맷 문자들로 구성되죠. 게다가 특수 문자들이 바이트 순서, 크기, 정렬을 제어해요. 각 포맷 문자열은 데이터의 전반적 속성을 설명하는 선택적 접두 문자와, 실제 데이터 값과 패딩을 설명하는 하나 이상의 포맷 문자로 구성돼요.
바이트 순서, 크기, 정렬
기본적으로 C 타입은 머신의 네이티브 포맷과 바이트 순서로 표현되고, 필요하면 패드 바이트를 건너뛰어 적절히 정렬돼요(C 컴파일러가 쓰는 규칙에 따라). 이 동작은 패킹된 구조체의 바이트가 대응하는 C 구조체의 메모리 레이아웃과 정확히 일치하도록 선택된 거예요. 네이티브 바이트 순서와 패딩을 쓸지 표준 포맷을 쓸지는 응용에 달려 있어요.
그 대신, 포맷 문자열의 첫 번째 문자로 패킹된 데이터의 바이트 순서, 크기, 정렬을 나타낼 수 있어요. 다음 표와 같아요:
| 문자 | 바이트 순서 | 크기 | 정렬 |
|---|---|---|---|
@ |
native | native | native |
= |
native | standard | none |
< |
little-endian | standard | none |
> |
big-endian | standard | none |
! |
network (= big-endian) | standard | none |
첫 번째 문자가 이것들 중 하나가 아니면 '@'로 간주돼요.
참고 — 숫자 1023(십육진수
0x3ff)는 다음 바이트 표현을 가져요: big-endian(>)에서는03 ff, little-endian(<)에서는ff 03.>>> import struct >>> struct.pack('>h', 1023) b'\x03\xff' >>> struct.pack('<h', 1023) b'\xff\x03'
네이티브 바이트 순서는 호스트 시스템에 따라 big-endian 또는 little-endian이에요. 예를 들어 Intel x86, AMD64(x86-64), Apple M1은 little-endian이고, IBM z와 많은 레거시 아키텍처는 big-endian이에요. 시스템의 endianness를 확인하려면 sys.byteorder를 쓰세요.
네이티브 크기와 정렬은 C 컴파일러의 sizeof 표현식으로 결정돼요. 이것은 항상 네이티브 바이트 순서와 결합돼요. 표준 크기는 포맷 문자에만 의존해요(Format Characters 섹션의 표 참고).
'@'와 '='의 차이를 주목하세요: 둘 다 네이티브 바이트 순서를 쓰지만, 후자의 크기와 정렬은 표준화되어 있어요. '!' 형태는 IETF RFC 1700에 정의된 대로 항상 big-endian인 네트워크 바이트 순서를 나타내요.
네이티브가 아닌 바이트 순서(강제 바이트 스와핑)를 나타내는 방법은 없어요 — '<' 또는 '>'를 적절히 선택해 쓰면 돼요.
참고 사항:
- 패딩은 연속된 구조체 멤버 사이에만 자동으로 추가돼요. 인코딩된 구조체의 시작이나 끝에는 패딩이 추가되지 않아요.
- 비-네이티브 크기와 정렬을 쓸 때(예:
<,>,=,!로)는 패딩이 추가되지 않아요. - 구조체의 끝을 특정 타입의 정렬 요건에 맞추려면, 그 타입에 대한 코드를 반복 횟수 0으로 포맷을 끝내요. Examples 섹션 참고.
포맷 문자
포맷 문자들은 다음 의미를 가져요. C와 Python 값 사이의 변환은 그 타입들을 보면 분명해요. 'Standard size' 열은 표준 크기를 쓸 때, 즉 포맷 문자열이 '<', '>', '!', '=' 중 하나로 시작할 때 패킹된 값의 바이트 크기를 가리켜요. 네이티브 크기를 쓸 때 패킹된 값의 크기는 플랫폼에 따라 달라요.
| 포맷 | C 타입 | Python 타입 | 표준 크기 | 참고 |
|---|---|---|---|---|
x |
pad byte | 값 없음 | (7) | |
c |
char | 길이 1의 bytes | 1 | |
b |
signed char | int | 1 | (2) |
B |
unsigned char | int | 1 | (2) |
? |
_Bool | bool | 1 | (1) |
h |
short | int | 2 | (2) |
H |
unsigned short | int | 2 | (2) |
i |
int | int | 4 | (2) |
I |
unsigned int | int | 4 | (2) |
l |
long | int | 4 | (2) |
L |
unsigned long | int | 4 | (2) |
q |
long long | int | 8 | (2) |
Q |
unsigned long long | int | 8 | (2) |
n |
ssize_t |
int | (2), (3) | |
N |
size_t |
int | (2), (3) | |
e |
_Float16 | float | 2 | (4), (6) |
f |
float | float | 4 | (4) |
d |
double | float | 8 | (4) |
F |
float complex | complex | 8 | (10) |
D |
double complex | complex | 16 | (10) |
s |
char[] | bytes | (9) | |
p |
char[] | bytes | (8) | |
P |
void* | int | (2), (5) |
버전 3.3에서 변경: 'n'과 'N' 포맷 지원 추가. 버전 3.6에서 변경: 'e' 포맷 지원 추가. 버전 3.14에서 변경: 'F'와 'D' 포맷 지원 추가.
더 알아보기 —
array와 ctypes 모듈, numpy 같은 서드파티 모듈은 비슷하지만 약간 다른 타입 코드를 사용해요.
참고 사항:
'?'변환 코드는 C99 이후 C 표준이 정의하는 _Bool 타입에 대응해요. 표준 모드에서 한 바이트로 표현돼요.- 정수 변환 코드 중 하나로 정수가 아닌 값을 패킹하려 할 때, 그 값에
__index__()메서드가 있으면 패킹 전에 그 메서드를 호출해 인자를 정수로 변환해요. 버전 3.2에서 변경: 정수가 아닌 값에__index__()메서드 사용 추가. 'n'과'N'변환 코드는 네이티브 크기(기본 또는'@'바이트 순서 문자로 선택)에서만 사용할 수 있어요. 표준 크기에서는 응용에 맞는 다른 정수 포맷 중 아무거나 쓸 수 있어요.'f','d','e'변환 코드의 경우 패킹된 표현은 플랫폼의 부동소수점 포맷과 무관하게 IEEE 754 binary32, binary64, binary16 포맷을 사용해요('f','d','e'각각에 해당).'P'포맷 문자는 네이티브 바이트 순서(기본 또는'@'바이트 순서 문자로 선택)에서만 쓸 수 있어요.'='바이트 순서 문자는 호스트 시스템에 따라 little- 또는 big-endian 순서를 쓰도록 선택해요. struct 모듈은 이것을 네이티브 순서로 해석하지 않으므로'P'포맷은 사용할 수 없어요.- IEEE 754 binary16 "반정밀도(half precision)" 타입은 IEEE 754 표준의 2008년 개정에서 도입됐어요. 부호 비트, 5비트 지수, 11비트 정밀도(10비트가 명시적으로 저장)를 가지며, 완전 정밀도로 약
6.1e-05~6.5e+04사이의 숫자를 표현할 수 있어요. 이 타입은 C 컴파일러가 널리 지원하지 않아요. 컴파일러가 C23 표준의 Annex H를 지원한다면 _Float16 타입으로 쓸 수 있어요. 전형적인 머신에서 부호 없는 short를 저장에는 쓸 수 있지만 수학 연산에는 쓸 수 없어요. 반정밀도 부동소수점 포맷에 대한 자세한 내용은 Wikipedia 페이지를 참고하세요. - 패킹할 때
'x'는 NUL 바이트 하나를 삽입해요. 'p'포맷 문자는 "Pascal 문자열"을 인코딩해요. 즉 count가 주는 고정된 바이트 수에 저장된 짧은 가변 길이 문자열이에요. 저장되는 첫 바이트는 문자열의 길이 또는 255 중 작은 쪽이에요. 그 뒤에 문자열의 바이트가 이어져요.pack()에 전달된 바이트 문자열이 너무 길면(count-1보다 길면), 문자열의 앞count-1바이트만 저장돼요. 바이트 문자열이count-1보다 짧으면 정확히 count 바이트가 사용되도록 null 바이트로 패딩돼요.unpack()의 경우'p'포맷 문자는count바이트를 소비하지만, 반환된bytes객체는 255바이트보다 많을 수 없다는 점을 주의하세요. 패킹할 때bytes와bytearray타입의 인자가 받아들여져요.'s'포맷 문자의 경우 count는 다른 포맷 문자처럼 반복 횟수가 아니라 바이트 문자열의 길이로 해석돼요. 예를 들어'10s'는 단일 Python 바이트 문자열로 매핑되는 단일 10바이트 문자열을 의미하는 반면,'10c'는 열 개의 Python 바이트 객체로 매핑되는 열 개의 별도 1바이트 문자 요소(예:cccccccccc)를 의미해요. count가 주어지지 않으면 기본값은 1이에요. 패킹할 때 바이트 문자열은 맞도록 잘리거나 null 바이트로 패딩돼요. 언패킹할 때 결과bytes객체는 항상 정확히 지정된 바이트 수를 가져요. 특수한 경우로'0s'는 단일 빈 바이트 문자열을 의미해요('0c'는 0 문자를 의미). 패킹할 때bytes와bytearray타입의 인자가 받아들여져요.'F'와'D'포맷 문자의 경우 패킹된 표현은 플랫폼의 부동소수점 포맷과 무관하게 복소수의 구성 요소에 IEEE 754 binary32와 binary64 포맷을 사용해요. C에서 복소수 타입이 선택적 기능임에도 불구하고 복소수 타입(F,D)은 무조건 사용할 수 있음을 주의하세요. C11 표준에 명시된 대로 각 복소수 타입은 각각 실수부와 허수부를 담는 두 요소짜리 C 배열로 표현돼요.
포맷 문자 앞에는 정수 반복 횟수를 붙일 수 있어요. 예를 들어 포맷 문자열 '4h'는 'hhhh'와 정확히 같아요. 포맷 사이의 공백 문자는 무시되지만, count와 그 포맷에는 공백이 있어선 안 돼요.
정수 포맷('b', 'B', 'h', 'H', 'i', 'I', 'l', 'L', 'q', 'Q') 중 하나로 값 x를 패킹할 때, x가 그 포맷의 유효 범위를 벗어나면 struct.error가 발생해요. 버전 3.1에서 변경: 이전에는 정수 포맷 중 일부가 범위 밖 값을 감싸고 struct.error 대신 DeprecationWarning을 발생시켰어요.
'?' 포맷 문자의 경우 반환 값은 True 또는 False예요. 패킹할 때 인자 객체의 진리값이 사용돼요. 네이티브 또는 표준 bool 표현의 0 또는 1이 패킹되고, 언패킹할 때 0이 아닌 값은 True가 돼요.
예제
참고 — 네이티브 바이트 순서 예제(
'@'포맷 접두 또는 접두 문자 없음)는 플랫폼과 컴파일러에 의존하므로 독자의 머신이 만들어내는 것과 일치하지 않을 수 있어요.
big endian 순서로 세 가지 다른 크기의 정수를 패킹하고 언패킹해요:
>>> from struct import *
>>> pack(">bhl", 1, 2, 3)
b'\x01\x00\x02\x00\x00\x00\x03'
>>> unpack('>bhl', b'\x01\x00\x02\x00\x00\x00\x03')
(1, 2, 3)
>>> calcsize('>bhl')
7
정의된 필드에 너무 큰 정수를 패킹하려 하면:
>>> pack(">h", 99999)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
struct.error: 'h' format requires -32768 <= number <= 32767
's'와 'c' 포맷 문자의 차이를 보여줘요:
>>> pack("@ccc", b'1', b'2', b'3')
b'123'
>>> pack("@3s", b'123')
b'123'
언패킹된 필드는 변수에 할당하거나 결과를 named tuple로 감싸 이름을 붙일 수 있어요:
>>> record = b'raymond \x32\x12\x08\x01\x08'
>>> name, serialnum, school, gradelevel = unpack('<10sHHb', record)
>>> from collections import namedtuple
>>> Student = namedtuple('Student', 'name serialnum school gradelevel')
>>> Student._make(unpack('<10sHHb', record))
Student(name=b'raymond ', serialnum=4658, school=264, gradelevel=8)
네이티브 모드에서는 패딩이 암시적이므로 포맷 문자의 순서가 크기에 영향을 줄 수 있어요. 표준 모드에서는 원하는 패딩을 직접 삽입하는 건 사용자의 책임이에요. 아래 첫 pack 호출에서 패킹된 '#' 뒤에 세 개의 NUL 바이트가 추가되어 다음 정수를 4바이트 경계에 정렬한 것을 주목하세요. 이 예제의 출력은 little endian 머신에서 만들어졌어요:
>>> pack('@ci', b'#', 0x12131415)
b'#\x00\x00\x00\x15\x14\x13\x12'
>>> pack('@ic', 0x12131415, b'#')
b'\x15\x14\x13\x12#'
>>> calcsize('@ci')
8
>>> calcsize('@ic')
5
다음 포맷 'llh0l'은 플랫폼의 long이 4바이트 경계에 정렬된다고 가정하면 끝에 패드 바이트 두 개가 추가돼요:
>>> pack('@llh0l', 1, 2, 3)
b'\x00\x00\x00\x01\x00\x00\x00\x02\x00\x03\x00\x00'
더 알아보기 —
array모듈: 균질 데이터의 패킹된 이진 저장.json모듈: JSON 인코더/디코더.pickle모듈: Python 객체 직렬화.
응용
struct 모듈의 두 가지 주요 응용이 있어요. 하나는 응용 내부 또는 같은 컴파일러로 컴파일된 다른 응용의 Python 코드와 C 코드 사이의 데이터 교환(네이티브 포맷)이고, 다른 하나는 합의된 데이터 레이아웃을 사용하는 응용들 사이의 데이터 교환(표준 포맷)이에요. 일반적으로 이 두 영역을 위해 구성하는 포맷 문자열은 서로 다르죠.
네이티브 포맷
네이티브 레이아웃을 흉내 내는 포맷 문자열을 만들 때는 컴파일러와 머신 아키텍처가 바이트 순서와 패딩을 결정해요. 이런 경우 @ 포맷 문자를 써서 네이티브 바이트 순서와 데이터 크기를 지정해야 해요. 내부 패드 바이트는 보통 자동으로 삽입돼요. 연속된 데이터 청크를 제대로 정렬하기 위해 올바른 바이트 경계로 올림하려면 포맷 문자열 끝에 0-반복 포맷 코드가 필요할 수 있어요.
이 두 간단한 예를(64비트 little-endian 머신에서) 봐요:
>>> calcsize('@lhl')
24
>>> calcsize('@llh')
18
두 번째 포맷 문자열의 끝에 추가 패딩 없이는 데이터가 8바이트 경계로 패딩되지 않아요. 0-반복 포맷 코드가 그 문제를 해결해요:
>>> calcsize('@llh0l')
24
'x' 포맷 코드로 반복을 지정할 수도 있지만, 네이티브 포맷에서는 '0l' 같은 0-반복 포맷을 쓰는 게 더 좋아요. 기본적으로 네이티브 바이트 순서와 정렬이 사용되지만, 명시적으로 '@' 접두 문자를 쓰는 게 더 좋아요.
표준 포맷
네트워킹이나 저장처럼 프로세스 밖으로 데이터를 주고받을 때는 정확해야 해요. 정확한 바이트 순서, 크기, 정렬을 지정하세요. 특정 머신의 네이티브 순서와 일치한다고 가정하지 마세요. 예를 들어 네트워크 바이트 순서는 big-endian인 반면 많은 인기 CPU는 little-endian이에요. 이것을 명시적으로 정의하면 코드가 실행되는 플랫폼의 세부사항을 신경 쓸 필요가 없어져요. 첫 번째 문자는 보통 < 또는 >(또는 !)여야 해요. 패딩은 프로그래머의 책임이에요. 0-반복 포맷 문자는 동작하지 않아요. 대신 필요하면 사용자가 명시적으로 'x' 패드 바이트를 추가해야 해요. 이전 섹션의 예제를 다시 보면:
>>> calcsize('<qh6xq')
24
>>> pack('<qh6xq', 1, 2, 3) == pack('@lhl', 1, 2, 3)
True
>>> calcsize('@llh')
18
>>> pack('@llh', 1, 2, 3) == pack('<qqh', 1, 2, 3)
True
>>> calcsize('<qqh6x')
24
>>> calcsize('@llh0l')
24
>>> pack('@llh0l', 1, 2, 3) == pack('<qqh6x', 1, 2, 3)
True
위의 결과(64비트 머신에서 실행)는 다른 머신에서 실행하면 일치한다고 보장할 수 없어요. 예를 들어 아래 예제들은 32비트 머신에서 실행됐어요:
>>> calcsize('<qqh6x')
24
>>> calcsize('@llh0l')
12
>>> pack('@llh0l', 1, 2, 3) == pack('<qqh6x', 1, 2, 3)
False
클래스
struct 모듈은 다음 타입도 정의해요.
class struct.Struct(format) — 포맷 문자열 format에 따라 이진 데이터를 쓰고 읽는 새 Struct 객체를 반환해요. Struct 객체를 한 번 만들고 그 메서드를 호출하는 것은 같은 포맷의 모듈-레벨 함수를 호출하는 것보다 효율적이에요. 포맷 문자열이 한 번만 컴파일되니까요.
참고 — 모듈-레벨 함수에 전달된 가장 최근 포맷 문자열의 컴파일된 버전이 캐시되므로, 몇 개의 포맷 문자열만 쓰는 프로그램은 단일
Struct인스턴스를 재사용할 걱정을 할 필요가 없어요.
컴파일된 Struct 객체는 다음 메서드와 속성을 지원해요.
pack(v1, v2, ...)— 컴파일된 포맷을 사용한다는 점만 빼고pack()함수와 동일해요(len(result)는size와 같을 거예요).pack_into(buffer, offset, v1, v2, ...)— 컴파일된 포맷을 사용한다는 점만 빼고pack_into()함수와 동일해요.unpack(buffer)— 컴파일된 포맷을 사용한다는 점만 빼고unpack()함수와 동일해요. 버퍼의 바이트 크기는size와 같아야 해요.unpack_from(buffer, offset=0)— 컴파일된 포맷을 사용한다는 점만 빼고unpack_from()함수와 동일해요. 위치offset에서 시작하는 버퍼의 바이트 크기는size이상이어야 해요.iter_unpack(buffer)— 컴파일된 포맷을 사용한다는 점만 빼고iter_unpack()함수와 동일해요. 버퍼의 바이트 크기는size의 배수여야 해요. (버전 3.4에 추가됨.)format— 이 Struct 객체를 만드는 데 사용된 포맷 문자열. 버전 3.7에서 변경: 포맷 문자열 타입이bytes대신str이 됨.size—format에 대응하는 구조체(pack()메서드가 만들어내는 bytes 객체)의 계산된 크기. 버전 3.13에서 변경: struct의 repr()이 바뀜. 이제 다음과 같아요:>>> Struct('i') Struct('i')