惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
MyScale Blog
MyScale Blog
Jina AI
Jina AI
爱范儿
爱范儿
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
I
Intezer
The Cloudflare Blog
T
Threat Research - Cisco Blogs
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Docker
AI
AI
Scott Helme
Scott Helme
Attack and Defense Labs
Attack and Defense Labs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
L
LangChain Blog
Recent Announcements
Recent Announcements
Security Latest
Security Latest
Hugging Face - Blog
Hugging Face - Blog
W
WeLiveSecurity
Last Week in AI
Last Week in AI
Security Archives - TechRepublic
Security Archives - TechRepublic
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Proofpoint News Feed
S
Securelist
S
Security Affairs
Project Zero
Project Zero
博客园 - 叶小钗
Google DeepMind News
Google DeepMind News
T
Tor Project blog
A
About on SuperTechFans
V2EX - 技术
V2EX - 技术
宝玉的分享
宝玉的分享
T
Tenable Blog
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
Simon Willison's Weblog
Simon Willison's Weblog
Forbes - Security
Forbes - Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
V2EX
AWS News Blog
AWS News Blog
The GitHub Blog
The GitHub Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
P
Privacy & Cybersecurity Law Blog
阮一峰的网络日志
阮一峰的网络日志
I
InfoQ
C
CXSECURITY Database RSS Feed - CXSecurity.com
H
Hacker News: Front Page
美团技术团队

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Невыразимое невалидное. Часть 1. Данные
Arutyun Tatulyan · 2026-06-19 · via Все публикации подряд на Хабре

Любой, кто писал конструктор запросов или фильтр над пользовательским вводом, знает, как это начинается. Сначала модель данных — это один простой класс, и кажется на этом всё. Через пару месяцев в нём value: Any, рядом валидатор строк на сто, и где-то посередине ветка, которую ты дописал ночью перед релизом, потому что кто-то прислал диапазон туда, где ждали число. Тесты зелёные: на эту комбинацию их просто не было. Падает, разумеется, в проде.

Как не довести до такой ситуации? Способ контринтуитивный: нужно не добавить ещё проверок, а наоборот — сделать так, чтобы неправильное состояние нельзя было даже собрать. Не запретить, а лишить возможности выразить.

У этой мысли есть формулировка, которую вы наверняка слышали: make illegal states unrepresentable, «сделай невалидные состояния невыразимыми». По индустрии её разнёс Ярон Мински из Jane Street, рассказывая, как они пишут на OCaml биржевую торговлю, где лишнее состояние в типе стоит реальных денег. Изначально эта идея из алгебраических типов ML и Haskell — и из обратного примера, который знают все.

Чтобы немного облачить дальнейшие идеи в практические примеры, всё дальше будет крутиться вокруг реального примера типа фильтров для дашборда. Пользователь собирает условие «поле — оператор — значение»: выручка больше тысячи, статус из списка, дата в промежутке. С фронта это прилетает JSON’ом, на бэкенде надо провалидировать, собрать параметризованный SQL и заодно показать человеку текстом, что он нафильтровал. Пока операторов три, всё прекрасно. Когда их два десятка, а типов полей пять, выясняется, что осмысленна хорошо если четверть сочетаний: contains к числу или > к списку — бессмыслица, которую модель тем не менее позволяет собрать.

В этой части будет про сами данные: как описать их так, чтобы мусор не выражался. Поведение (как всё это превратить в SQL и текст), разговор про потолок типов оставим на вторую.

Как обычно начинают (наивная реализация)

Первая версия почти всегда выглядит так:

from enum import StrEnum
from typing import Any
from pydantic import BaseModel, model_validator

class Operator(StrEnum):
    EQ = "="
    LT = "<"
    GT = ">"
    IN_RANGE = "in_range"
    IN = "in"
    CONTAINS = "contains"

class Predicate(BaseModel):
    field: Field
    operator: Operator
    value: Any

Узкое место — value: Any. Оператор < хочет число или дату, in_range — пару границ, in — список строк, contains — строку. Типы про это не знают ничего, поэтому связь «оператор ↔ форма значения» приходится тащить руками:

    @model_validator(mode="after")
    def _check(self) -> Predicate:
        if self.operator is Operator.IN_RANGE:
            if not (isinstance(self.value, (list, tuple)) and len(self.value) == 2):
                raise ValueError("in_range требует пару [min, max]")
        elif self.operator in (Operator.LT, Operator.GT):
            if not isinstance(self.value, (int, float, date)):
                raise ValueError(f"{self.operator} требует число или дату")
        elif self.operator is Operator.IN:
            if not (isinstance(self.value, list) and all(isinstance(x, str) for x in self.value)):
                raise ValueError("in требует список строк")
        # ... и так для каждого нового оператора и каждого типа поля
        return self

Этот валидатор по сути самопальная система типов. Только хуже настоящей: работает в рантайме, и её ещё надо не забыть вызвать. И, главное, саму модель он не лечит. Predicate(operator=Operator.LT, value=["a", "b"]) по-прежнему отлично конструируется — value: Any, чекер слеп. Ошибку поймает либо рантайм, либо ревьюер, либо никто. Чем больше операторов и типов полей, тем длиннее простыня из if, и тем дальше по коду протекает value: Any: каждая следующая функция, получив Predicate, снова не знает, что там внутри, и снова сужает сама. За тем, что удаётся увести в типы, а что остаётся проверкой на рантайме, дальше и любопытно следить.

Перечислить варианты вместо одного «на всё»

Развернём всё ровно наоборот. Вместо одного типа, который умеет всё и оттого не гарантирует ничего, выпишем узкие варианты и в каждом намертво свяжем операторную группу с типом значения:

import typing
from pydantic import BaseModel

class NumericPredicate(BaseModel):
    kind: typing.Literal["numeric"] = "numeric"
    field: Field
    operator: EqualityOperator | OrderOperator     # =, !=, <, >, <=, >=
    value: float

class NumericRangePredicate(BaseModel):
    kind: typing.Literal["numeric_range"] = "numeric_range"
    field: Field
    operator: RangeOperator                          # in_range, out_of_range
    value: Range[float]

class SetPredicate(BaseModel):
    kind: typing.Literal["set"] = "set"
    field: Field
    operator: SetOperator                            # in, not_in
    value: list[str]

NumericRangePredicate не примет одиночное число: value у него Range[float], и обойти это нельзя. Пара «диапазонный оператор + скаляр» теперь не «запрещена валидатором» — её невозможно набрать руками. Невалидное перестало быть выразимым.

Операторы тоже разнесены по маленьким группам, а не свалены в один enum:

class EqualityOperator(StrEnum):
    EQUAL = "="
    NOT_EQUAL = "!="

class OrderOperator(StrEnum):
    LESS_THAN = "<"
    GREATER_THAN = ">"
    ...

class RangeOperator(StrEnum):
    IN_RANGE = "in_range"
    OUT_OF_RANGE = "out_of_range"

class SetOperator(StrEnum):
    IN = "in"
    NOT_IN = "not_in"

Таких вариантов получается семь: числовой, числовой-диапазон, дата, дата-диапазон, текст, категория, множество. Если захочется терминов, то старая модель была произведением типов «любой оператор × любое значение», декартов квадрат, где осмысленны единицы клеток. Новая же суммой: допустимых состояний ровно столько, сколько выписано. В Rust это enum, в Haskell data, в TypeScript discriminated union; в Python мы только что собрали то же самое почти вручную.

Пространство представимых состояний: до и после

Пространство представимых состояний: до и после

Pydantic в доменной модели?

Здесь можно погрузиться в осуждение, ведь pydantic это про валидацию и сериализацию, инфраструктура, а доменным типам положено быть чистыми, без сторонних зависимостей. Претензия резонная, и до известной границы я её разделяю.

Но pydantic v2 я тут использую не как «валидатор на входе», а как способ описать форму данных. Дискриминированные union’ы, ограничения в Annotated, инварианты прямо рядом с полями. На голых dataclass всё это пришлось бы писать руками и получить тот же pydantic, только свой и с собственными багами. По мне, размен честный: одна зависимость (которую проект всё равно тянет в моем случае) в обмен на невалидные состояния, которые просто не собираются. А если и так перебор, то почти всё переносится на attrs или ручные проверки, будет лишь многословнее.

Один инвариант в типах Python всё равно не выразишь, нельзя проверить, что тип поля совпадает с вариантом предиката (нельзя нацепить числовой предикат на текстовую колонку). Его и оставляем рантайму, в общей базе:

class _PredicateBase(BaseModel):
    _field_types: typing.ClassVar[frozenset[FieldType]]
    field: Field

    @model_validator(mode="after")
    def _check_field_type(self) -> typing.Self:
        if self.field.type not in self._field_types:
            raise ValueError(f"{type(self).__name__} не применим к полю {self.field.type}")
        return self

На этом валидатор и кончился. Стострочную простыню заменила одна проверка, а пара «оператор–значение» держится теперь типами, а не дисциплиной.

Собрать варианты в один тип

Семь классов это хорошо, но наружу хочется отдавать один тип Predicate, который умеет (де)сериализоваться. Собираем их в union с дискриминатором:

Predicate = typing.Annotated[
    NumericPredicate | NumericRangePredicate | DatePredicate | DateRangePredicate
    | TextPredicate | CategoryPredicate | SetPredicate,
    Field(discriminator="kind"),
]

То самое поле kind: Literal["numeric"], которое я молча приписал каждому варианту, — это и есть дискриминатор, тег. Literal["numeric"] это тип, у которого допустимо ровно одно значение: строка "numeric", не какая-то строка, а именно эта. Он играет роль связующего звена между значением и типом: по значению поля чекер точно знает, с каким вариантом имеет дело.

И тег решает, как union разбирается из JSON. Без него pydantic, получив словарь, примеряет варианты по очереди, пока какой-нибудь не подойдёт: медленно (в худшем случае перебор всех), неоднозначно (под одни данные могут подойти два варианта), и ошибки выходят мутные, без намёка, что не так. С дискриминатором гадания нет: pydantic смотрит на kind, сразу берёт нужный вариант и валидирует только его.

Дискриминатор против перебора вариантов

Дискриминатор против перебора вариантов

Пара мелочей, которые приятно знать. Такие union’ы вкладываются: если предикаты объединяются в булево дерево (узлы AND/OR/NOT, предикаты в листьях), это дерево тоже размечено по kind, и Predicate внутри него размечен сам, pydantic разворачивает вложенную диспетчеризацию без ручной склейки. И ещё: union это не модель, напрямую через Model.model_validate его не провалидируешь; для таких «не-моделей» есть TypeAdapter, оборачивающий любой тип (union, list[str], Range[float]) в готовый валидатор.

Строка, которая не бывает пустой

Варианты собраны и разбираются однозначно, но точность на этом не кончается. Сплошь и рядом нужно поле, которому нельзя быть пустым, имя длиннее нуля символов, email, который хоть как-то похож на email. В лоб это пишут проверкой в коде или валидатором на каждый класс. Но ограничение это часть типа, и его можно туда встроить:

from typing import Annotated
from pydantic import Field

NonEmptyString = Annotated[str, Field(min_length=1)]
DisplayName    = Annotated[str, Field(min_length=1, max_length=200)]
Email          = Annotated[str, Field(pattern=r"^[^@\s]+@[^@\s]+\.[^@\s]+$")]
FieldName      = Annotated[str, Field(pattern=r"^[a-z][a-z0-9_]*$")]

Annotated[str, ...] это всё ещё str, но с приклеенным условием. Теперь display_name: DisplayName в модели сам требует непустую строку до двухсот символов, без отдельной проверки в теле класса. Тип стал у́же: вместо «любая строка» — «строка, удовлетворяющая предикату».

У этого есть точное имя — refinement-тип, уточняющий тип: базовый тип плюс предикат, вырезающий из него подмножество. NonEmptyString — это { s : str | len(s) ≥ 1 }, формально подмножество всех строк.

Уточняющий тип как подмножество базового

Уточняющий тип как подмножество базового

В Python это refinement для бедных. Проверку делает pydantic в рантайме, в момент создания модели. А тайп-чекер за NonEmptyString видит обычный str — для mypy Annotated[str, ...] и str неразличимы. Гарантия настоящая, но появляется она на границе валидации, а не при компиляции. Для большинства задач этого ровно достаточно; но запомните это место, на нём будет держаться вторая часть статьи.

Range, но не для чего попало

Диапазон нужен и числам, и датам — но не произвольному T. Сравнивать на «меньше-больше» осмысленно у упорядоченных вещей, и тип может это прямо потребовать. В синтаксисе дженериков PEP 695 (Python 3.12+) выходит так:

class Range[T: (float, datetime.date)](BaseModel):
    start: T
    end: T

Тонкость, на которой спотыкаются, — в скобках. T: (float, datetime.date) — это не верхняя граница, а ограничение. Разница важная:

  • Граница T: SomeBase читается как «T — это SomeBase или любой его наследник»: подойдёт что угодно ниже по иерархии.

  • Ограничение T: (float, date) читается как «T — это ровно float или ровно date, третьего не дано»: список закрыт.

То есть Range[float] и Range[date] существуют, а Range[str] чекер не пропустит. По сути это маленькая сумма-типов, только на уровне параметров: T пробегает закрытый список.

А упорядоченность снова достаётся рантайму. Что start ≤ end, типами Python не выразить, для этого нужны типы, зависящие от значений, поэтому это опять проверка при создании:

    @model_validator(mode="after")
    def _check_order(self) -> Self:
        if self.end < self.start:
            raise ValueError("начало диапазона не должно превышать конец")
        return self

Что в итоге

Данные стали точными. Вместо одного типа с value: Any мы получили сумму узких вариантов, где неправильную пару «оператор–значение» просто негде написать; тег делает разбор однозначным; строки умеют быть непустыми; Range параметризован, но не чем попало.

Часть гарантий мы увели в типы — но не все. Тип поля совпадает с вариантом, min_length, start ≤ end это по-прежнему проверки в рантайме. И про поведение мы пока не сказали ни слова: фильтр умеет существовать, но не превращаться в SQL или текст. И то, и другое — во второй части: куда вешать поведение (и почему это целая «проблема выражения»), как не дать рантайм-проверкам тихо разъехаться и где вообще проходит потолок системы типов.