3.2.2.1. Sequence#
A sequence functions independently of fields. The library introduces the Sequence
as a named finite collection of Field objects. A Sequence
operates on a model, which is a string-to-field mapping by default. Later, we will discuss
the distinctions between a Sequence and a Struct regarding the model representation.
A sequence definition entails the specification of a Sequence object by directly
indicating the model to use. Inheritance poses a challenge with sequences, as they are not
designed to operate on a type hierarchy. The default instantiation with all default options
involves passing the dictionary with all fields directly:
>>> Format = Sequence({"a": uint8, "b": uint32})
Programmers Note:
All sequence types introduced by this library can also store so-called unnamed fields.
These fields are not visible in the unpacked result and are automatically packed, removing
concerns about them when the option S_DISCARD_UNNAMED is active. Their names usually
begin with an underscore and must solely contain numbers (e.g., _123).
The sequence follows the Field configuration model, allowing sequence and
field-related options to be set. As mentioned earlier, the S_DISCARD_UNNAMED option can
be used for example to exclude all unnamed fields from the final representation. A complete
list of all configuration options and their impact can be found in Options / Flags.
All sequences store a configurable ByteOrder and Arch as architecture,
which are passed to all fields in the current model. For more information on why these
classes are not specified as an enum class, please refer to Byteorder and Architecture.
Inheritance in sequences is intricate, as a Sequence is constructed from a dictionary
of elements and has no Python type hierarchy. The legacy + and - operators mutate the
left-hand sequence by importing or removing fields in its internal representation. They are kept
for compatibility and can be useful for one-off transformations, but they are not safe base-sequence
composition helpers. For instance:
>>> BaseFormat = Sequence({"magic": b"MAGIC", "a": uint8})
>>> Format = Sequence({"b": uint32, "c": uint16}) + BaseFormat
will result in the following field order:
>>> list(Format.get_members())
['b', 'c', 'magic', 'a']
which is not the intended order. The correct order should be ['magic', 'a', 'b', 'c'].
This can be achieved by using the BaseFormat instance as the first operand.
Warning
Using BaseFormat + Extension will alter the BaseFormat sequence, making it unusable
elsewhere as the unchanged base for other formats. Prefer merged and without for
reusable sequence composition, or use Struct when the format
naturally follows a type hierarchy.
For non-mutating composition, use merged()
and without(). These helpers clone imported
field wrappers and leave the original sequences unchanged:
>>> BaseFormat = Sequence({"magic": b"MAGIC", "a": uint8})
>>> Extension = Sequence({"b": uint32})
>>> Format = BaseFormat.merged(Extension)
>>> list(BaseFormat.get_members())
['magic', 'a']
>>> list(Format.get_members())
['magic', 'a', 'b']
merged starts with a clone of the receiver and imports clones from each
additional sequence. Duplicate field names from later sequences replace earlier
ones in-place in the returned layout. without starts with a clone of the
receiver and removes fields by name, again leaving all source sequences intact.
Nesting sequences is allowed by default and can be achieved by incorporating another
Sequence into the model. It is important to note that nesting is distinct from
inheritance, adding an additional layer of packing and unpacking.
>>> Format = Sequence({"other": BaseFormat, "b": uint32})