Списки и вложенные модели

Несколько значений одного типа

Пользователь может состоять в нескольких группах:

from bytespec import ProtoModel, field

class User(ProtoModel):
    name: str
    groups: list[str]

user = User(name="Anna", groups=["readers", "editors"])
decoded = User.decode(user.encode())
print(decoded.groups)  # ['readers', 'editors']

list[str] означает список строк. Так же работают list[UInt32] и списки других поддерживаемых типов. Список может быть пустым.

Отдельный пустой список для каждого пользователя

Чтобы не передавать groups=[] вручную, используйте default_factory:

class User(ProtoModel):
    name: str
    groups: list[str] = field(default_factory=list)

anna = User(name="Anna")
boris = User(name="Boris")
anna.groups.append("editors")

print(anna.groups)   # ['editors']
print(boris.groups)  # []

Фабрика list вызывается без аргументов при создании каждого экземпляра, если поле не передали. Так пользователи получают разные списки. default=[] вместо этого использовал бы один общий объект.

Фабрикой может быть и другая функция без аргументов: например, uuid4 для поля типа UUID. Она не вызывается при объявлении класса. Задавайте либо default, либо default_factory — одновременно нельзя. Правила проверки их значений собраны в Defaults и создание экземпляра.

Модель как поле другой модели

Используем уже объявленный User как автора сообщения:

class Message(ProtoModel):
    author: User
    text: str

message = Message(author=anna, text="Hello!")
decoded = Message.decode(message.encode())
print(decoded.author.name)    # Anna
print(decoded.author.groups)  # ['editors']
print(decoded.text)           # Hello!

Передавайте экземпляр User, а не словарь. При чтении Message библиотека сама восстановит вложенный User. Объявляйте вложенный класс раньше того, который его использует: аннотации разрешаются сразу.

Список моделей

Для нескольких пользователей достаточно list[User]:

class Packet(ProtoModel):
    users: list[User]

packet = Packet(users=[anna, boris])
decoded = Packet.decode(packet.encode())
print([user.name for user in decoded.users])  # ['Anna', 'Boris']

Поддерживаются и вложенные списки, например list[list[str]]. Сам список можно сделать optional-полем: groups: list[str] | None = field(flag=0). При этом None и [] будут разными значениями. Optional-элементы list[str | None] не поддерживаются, как и автоматическая сериализация dict, tuple и set.

Вложенная модель сохраняет своё framing, пропуская только constructor: её тип уже известен из аннотации. Настройки внешнего класса не заменяют настройки вложенного. Точные байты разобраны позже в Как записывается header вложенной модели.

Обычные списки и вложенные модели не требуют настройки формата. Если нужно изменить кодировку текста или префикс длины, переходите к Строки, длины и настройки полей. Полное устройство вложенных данных описано отдельно в Binary / wire format.