Ei vielä käännetty
Tätä sivua ei ole vielä käännetty suomeksi, joten se näytetään englanniksi. Auta kääntämään
struct Module Complexity¶
The struct module handles binary data conversions, packing Python values into bytes and unpacking bytes into Python values using format strings.
Complexity Reference¶
| Operation | Time | Space | Notes |
|---|---|---|---|
Struct() compilation |
O(m) | O(m) | m = format string length; compile once |
pack() |
O(k + B) | O(B) | k = fields, B = total byte width. Output is B bytes however few fields produce it. The module caches compiled formats, so a repeated format is not re-parsed |
unpack() |
O(k + B) | O(k + B) | The tuple holds k elements, but byte-string fields carry their bytes with them |
pack_into() |
O(k + B) | O(1) | Writes into a caller-supplied buffer, so no output is allocated |
unpack_from() |
O(k + B) | O(k + B) | Same result space as unpack() |
calcsize() |
O(m) | O(1) | m = format string length |
Format Strings¶
Character Types¶
import struct
# Format string characters
# 'b' = signed char (1 byte)
# 'B' = unsigned char (1 byte)
# 'h' = signed short (2 bytes)
# 'H' = unsigned short (2 bytes)
# 'i' = signed int (4 bytes)
# 'I' = unsigned int (4 bytes)
# 'l' = signed long (4 bytes)
# 'L' = unsigned long (4 bytes)
# 'q' = signed long long (8 bytes)
# 'Q' = unsigned long long (8 bytes)
# 'f' = float (4 bytes)
# 'd' = double (8 bytes)
# 's' = char[] (variable)
# 'p' = pascal string (variable)
# 'P' = void* (pointer)
# Calculate size - O(m) where m = format length
size = struct.calcsize('i') # 4 bytes
size = struct.calcsize('ihh') # 8 bytes (4+2+2)
size = struct.calcsize('ihhf') # 12 bytes (4+2+2+4)
Byte Order and Alignment¶
import struct
# Byte order prefix (optional, default='@' native)
# '@' = native (default)
# '=' = native (no alignment)
# '<' = little-endian
# '>' = big-endian
# '!' = network (big-endian)
# Native order - O(m) parse
native = struct.calcsize('i') # 4
# Little-endian - O(m) parse
little = struct.calcsize('<i') # 4
# Big-endian - O(m) parse
big = struct.calcsize('>i') # 4
# With alignment - O(m) parse
aligned = struct.calcsize('@ii') # 8 (with padding)
unaligned = struct.calcsize('=ii') # 8 (no padding)
Packing Data¶
Simple Packing¶
import struct
# Pack single value - O(1)
# Format: integer (4 bytes)
bytes_data = struct.pack('i', 42)
print(bytes_data) # b'*\x00\x00\x00' (little-endian)
# Pack multiple values - O(n) for n values
bytes_data = struct.pack('ihh', 100, 200, 300)
# 4 bytes (int) + 2 bytes (short) + 2 bytes (short) = 8 bytes
# Pack with byte order - O(n)
bytes_data = struct.pack('>i', 42) # Big-endian
bytes_data = struct.pack('<i', 42) # Little-endian
String Packing¶
import struct
# Pack fixed-length string - O(n)
text = "Hello"
bytes_data = struct.pack('5s', text.encode()) # 5-byte string
# Pack with padding
name = "Bob"
bytes_data = struct.pack('10s', name.encode()) # Padded to 10 bytes
# Pack multiple strings
bytes_data = struct.pack('5s3s', b"Hello", b"Bob") # 8 bytes total
Pack Into Buffer¶
import struct
# Pack into existing buffer - O(n) for n fields
buffer = bytearray(20)
# Write at offset 0
struct.pack_into('i', buffer, 0, 42)
# Write at offset 4
struct.pack_into('h', buffer, 4, 100)
# Write at offset 6
struct.pack_into('f', buffer, 6, 3.14)
print(buffer[:10]) # First 10 bytes with packed data
Unpacking Data¶
Simple Unpacking¶
import struct
# Pack first
bytes_data = struct.pack('ihhf', 100, 200, 300, 3.14)
# Unpack all - O(n)
values = struct.unpack('ihhf', bytes_data)
print(values) # (100, 200, 300, 3.140000104904175)
# Unpack specific subset
values = struct.unpack('ih', bytes_data[:6]) # Skip last values
print(values) # (100, 200)
Unpack From Buffer¶
import struct
# Create buffer with packed data
buffer = bytearray(20)
struct.pack_into('i', buffer, 0, 42)
struct.pack_into('h', buffer, 4, 100)
struct.pack_into('f', buffer, 6, 3.14)
# Unpack from buffer - O(n)
value1 = struct.unpack_from('i', buffer, 0)[0] # 42
value2 = struct.unpack_from('h', buffer, 4)[0] # 100
value3 = struct.unpack_from('f', buffer, 6)[0] # 3.14
Struct Objects (Compiled Format)¶
Create and Reuse Struct¶
import struct
# Create compiled struct - O(m) once, then O(n) per operation
header_format = struct.Struct('4sI') # 4-char string + unsigned int
# Pack with compiled struct - O(n) for n fields
bytes_data = header_format.pack(b"HEAD", 12345)
# Unpack with compiled struct - O(n) for n fields
header, version = header_format.unpack(bytes_data)
print(header) # b'HEAD'
print(version) # 12345
# Size calculation - O(1)
size = header_format.size # 8
Struct for Network Protocol¶
import struct
# Message format: type(1) + length(2) + timestamp(4) + data
class Message:
HEADER_FORMAT = struct.Struct('!BHI') # Network byte order
def __init__(self, msg_type, timestamp, data):
self.type = msg_type
self.timestamp = timestamp
self.data = data
def serialize(self):
"""Pack to bytes - O(n)"""
header = self.HEADER_FORMAT.pack(
self.type,
len(self.data),
self.timestamp
)
return header + self.data
@classmethod
def deserialize(cls, data):
"""Unpack from bytes - O(n)"""
header_size = cls.HEADER_FORMAT.size
msg_type, length, timestamp = cls.HEADER_FORMAT.unpack(
data[:header_size]
)
payload = data[header_size:header_size + length]
return cls(msg_type, timestamp, payload)
# Usage
msg = Message(1, 1234567890, b"Hello")
bytes_msg = msg.serialize()
msg2 = Message.deserialize(bytes_msg)
Common Patterns¶
Binary File I/O¶
import struct
class BinaryWriter:
"""Write binary data to file"""
def __init__(self, filename):
self.file = open(filename, 'wb')
def write_int(self, value):
"""Write integer - O(1)"""
self.file.write(struct.pack('i', value))
def write_string(self, value, length):
"""Write fixed-length string - O(n)"""
self.file.write(struct.pack(f'{length}s', value.encode()))
def close(self):
self.file.close()
class BinaryReader:
"""Read binary data from file"""
def __init__(self, filename):
self.file = open(filename, 'rb')
def read_int(self):
"""Read integer - O(1)"""
return struct.unpack('i', self.file.read(4))[0]
def read_string(self, length):
"""Read fixed-length string - O(n)"""
return struct.unpack(f'{length}s', self.file.read(length))[0]
def close(self):
self.file.close()
# Usage
writer = BinaryWriter("data.bin")
writer.write_int(42)
writer.write_string("Hello", 10)
writer.close()
reader = BinaryReader("data.bin")
value = reader.read_int()
text = reader.read_string(10)
reader.close()
Network Packet Parsing¶
import struct
class PacketHeader:
"""Parse binary packet header"""
FORMAT = struct.Struct('!HHBBHH') # Network byte order
# Destination port (H)
# Source port (H)
# Sequence (B)
# Flags (B)
# Window (H)
# Checksum (H)
def __init__(self, data):
"""Parse header - O(1)"""
if len(data) < self.FORMAT.size:
raise ValueError("Insufficient data")
self.dst_port, self.src_port, self.seq, self.flags, \
self.window, self.checksum = self.FORMAT.unpack(
data[:self.FORMAT.size]
)
def to_bytes(self):
"""Serialize header - O(1)"""
return self.FORMAT.pack(
self.dst_port, self.src_port, self.seq,
self.flags, self.window, self.checksum
)
# Parse packet
packet_data = b'\x00P\x00P\x01\x00\x00\x10\x12\x34'
header = PacketHeader(packet_data)
print(f"Dst: {header.dst_port}, Src: {header.src_port}")
C Structure Mapping¶
import struct
# Map C struct: typedef struct { int id; float score; } Result;
class CResult:
"""Map to C struct"""
STRUCT_FORMAT = struct.Struct('if') # int + float
def __init__(self, id=0, score=0.0):
self.id = id
self.score = score
def pack(self):
"""Convert to C struct bytes - O(1)"""
return self.STRUCT_FORMAT.pack(self.id, self.score)
@classmethod
def unpack(cls, data):
"""Create from C struct bytes - O(1)"""
id, score = cls.STRUCT_FORMAT.unpack(data)
return cls(id, score)
# Usage
result = CResult(42, 95.5)
data = result.pack()
result2 = CResult.unpack(data)
Performance Considerations¶
Time Complexity¶
All timings on this page are from CPython 3.11 on one Linux machine and are illustrative, not portable.
- pack(): O(k + B) for k fields and B bytes. Field count carries the
larger constant:
'100i'and'400s'both produce 400 bytes, and the hundred-field version measured 5.2x the single-field one - but'40000s'is one field and still costs 9.6x'400s', so B is not negligible - unpack(): O(k + B), and the returned tuple holds the bytes too
- Struct creation: O(m) one-time cost (m = format length)
- pack_into(): O(k + B) time, O(1) extra space
Space Complexity¶
- Packed bytes: O(B) produced by
pack(), or none bypack_into() - Unpacked tuple: O(k + B) returned by
unpack(), since byte-string fields carry their bytes
Optimization Tips¶
The module-level functions keep a cache of compiled formats, so a format you use repeatedly is parsed once whether or not you pre-compile it. Pre-compiling saves the cache lookup, which is worth about 15%.
import struct
# Fine: the format is parsed once and then found in the cache
for i in range(1000):
data = struct.pack('i', i) # 67 ns per call
# Slightly better: no cache lookup either
int_struct = struct.Struct('i')
for i in range(1000):
data = int_struct.pack(i) # 58 ns per call
The saving is larger when the format is not in the cache, but only if the
Struct is reused. Sweeping 58 distinct formats, with _clearcache() called
each time - a forced cold cache, not natural capacity churn:
| µs per sweep | |
|---|---|
| Module call, cold cache | 46.2 |
Build a Struct then pack, per format |
42.9 |
Reuse Struct objects built beforehand |
14.2 |
Pre-compiling a format you use once saves nothing: you pay the same parse,
just at a different moment. The 3x is for reuse. So build your Struct
objects where you build the rest of your constants, and do not bother for a
one-shot format.
Format Modifiers¶
Repetition¶
import struct
# Pack 10 integers - O(n)
data = struct.pack('10i', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
# Unpack 10 integers - O(n)
values = struct.unpack('10i', data)
print(len(values)) # 10
# With byte order
data = struct.pack('>10H', *range(10)) # 10 big-endian shorts
Error Handling¶
import struct
# Format must match data size
try:
struct.unpack('i', b'AB') # O(m) - size check fails before any unpacking
except struct.error as e:
print(f"Unpack error: {e}")
# Invalid format character
try:
struct.pack('z', 42) # O(m) - rejected while parsing the format
except struct.error as e:
print(f"Pack error: {e}")
# Both failures cost format parsing only. Note that the module-level
# functions cache compiled formats, so a repeated format is not re-parsed on
# every call - pre-compiling a Struct saves the cache lookup, worth about
# 15% here, and saves the parse itself only when the cache misses