Наследование моделей¶
Чтобы несколько сообщений использовали одинаковое framing, вынесите настройки в базовый класс, а поля объявите в конкретных моделях:
from bytespec import Constructor, Flags, PayloadLength, ProtoModel, SchemaError
from bytespec.types import UInt8
class Compact(ProtoModel):
__header__ = (Constructor(1), PayloadLength(1))
class Reading(Compact):
__constructor__ = 7
value: UInt8
reading = Reading(value=42)
assert Reading.decode(reading.encode()) == reading
print(reading.encode().hex(" "))
07 01 2a
Наследуются __header__, __constructor__, __byte_order__ и правила
configure_codecs(). Framing-настройки можно переопределить и в подклассе
без собственных полей: каждый подкласс получает заново собранную схему.
Несколько базовых классов¶
При отсутствии явного __header__ учитываются custom headers прямых баз
ProtoModel. Стандартный header не конкурирует с пользовательским:
class DefaultBase(ProtoModel):
pass
class Mixed(DefaultBase, Compact):
value: UInt8
assert Mixed.__header__ is Compact.__header__
assert Mixed.decode(Mixed(value=1).encode()) == Mixed(value=1)
Diamond inheritance одного header тоже допустим:
class Left(Compact):
pass
class Right(Compact):
pass
class Diamond(Left, Right):
value: UInt8
assert Diamond.__header__ is Compact.__header__
assert Diamond.decode(Diamond(value=2).encode()) == Diamond(value=2)
Два разных custom headers требуют явного выбора:
class Other(ProtoModel):
__header__ = (PayloadLength(2),)
try:
class Conflict(Compact, Other):
value: UInt8
except SchemaError as error:
print(error)
class Resolved(Compact, Other):
__header__ = Compact.__header__
value: UInt8
assert Resolved.decode(Resolved(value=3).encode()) == Resolved(value=3)
Conflict: conflicting inherited headers; define __header__ explicitly
Сравнивается идентичность tuple, а не эквивалентность элементов.
Два независимо созданных (Constructor(1), PayloadLength(1)) конфликтуют
даже при одинаковом wire format. Чтобы переиспользовать один header,
наследуйте его или присвойте существующий tuple. Пустой tuple () тоже
считается custom header. Специальное объединение применяется только к
header; остальные настройки следуют обычному порядку поиска Python.
Поля не объединяются¶
Подкласс с собственными полями строит схему только из них:
class Parent(ProtoModel):
__header__ = ()
first: UInt8
class Child(Parent):
second: UInt8
child = Child(second=2)
print(child.encode().hex(" "))
02
first в схему Child не попадает. Для структуры «родительские данные
плюс новые данные» используйте Списки и вложенные модели или явно объявите весь
набор полей дочерней модели.
Изменить framing, сохранив поля¶
Подкласс без собственных сериализуемых полей сохраняет унаследованные поля,
их codecs, defaults и factories. Для него создаётся отдельная схема с его
__constructor__, __header__ и __byte_order__. Повторять поля
ради смены framing не нужно:
from bytespec import ByteOrder
from bytespec.types import UInt16
class ReadingBase(ProtoModel):
__constructor__ = 2
__header__ = (Constructor(1), PayloadLength(1))
value: UInt16
class LittleReading(ReadingBase):
__constructor__ = 3
__header__ = (PayloadLength(1), Constructor(2))
__byte_order__ = ByteOrder.LITTLE
reading = LittleReading(value=0x1234)
assert LittleReading.decode(reading.encode()) == reading
print(reading.encode().hex(" "))
print(ReadingBase(value=0x1234).encode().hex(" "))
02 03 00 34 12
02 02 12 34
В первой строке длина идёт перед двухбайтовым constructor, а число записано
little-endian. Формат родителя не изменился. Если в унаследованных полях есть
optional, новый header тоже должен содержать Flags достаточной ширины:
иначе объявление подкласса завершается SchemaError.
При multiple inheritance без собственных полей используется первый доступный набор полей по MRO, без объединения схем базовых классов. Правило выбора custom header при этом остаётся описанным выше.
Автоматический вызов __validate__ имеет отдельное правило: метод
родителя сам не вызывается для дочернего экземпляра. Как явно переиспользовать
проверку — Проверка значений модели.