Форум программистов, компьютерный форум, киберфорум
py-thonny
Войти
Регистрация
Восстановить пароль

Протоколы в Python

Запись от py-thonny размещена 31.10.2025 в 20:39
Показов 3415 Комментарии 0

Нажмите на изображение для увеличения
Название: Протоколы в Python.jpg
Просмотров: 112
Размер:	118.8 Кб
ID:	11359
Традиционная утиная типизация работает просто: попробовал вызвать метод, получилось - отлично, не получилось - упал с ошибкой в рантайме. Протоколы добавляют сюда проверку на этапе статического анализа. Пишешь код, запускаешь mypy - и сразу видишь, что твой класс не соответствует ожидаемому контракту. До запуска программы. Я столкнулся с этим год назад при рефакторинге высоконагруженного API. Там был хаос: классы наследовались друг от друга без явной необходимости, просто потому что "так надо для типов". Переписал на протоколы - и зависимости рассыпались как карточный домик. Каждый модуль стал независимым. Тесты упростились раза в три, потому что моки создавать стало элементарно.

Протоколы - это формализация того, что Python делал всегда. Раньше мы полагались на документацию и надежду, что разработчик не накосячит. Теперь можно записать ожидания в коде. Написал протокол - и все понятно: вот методы, вот сигнатуры, вот что должен уметь твой класс. Инструменты статического анализа подхватывают это автоматически и ругаются, если что-то не так.

Это не замена абстрактным базовым классам. Это другой инструмент для других задач. Там, где нужна жесткая иерархия и общее поведение - используй ABC. Где нужна гибкость и независимость компонентов - бери протоколы. Я предпочитаю второе, потому что связанность кода - главный враг поддерживаемости.

Отличие протоколов от абстрактных базовых классов и интерфейсов



Абстрактные базовые классы требуют явного наследования. Ты пишешь class MyLogger(BaseLogger): - и теперь MyLogger навсегда привязан к BaseLogger. Хочешь проверить типы - обязан унаследоваться. Протоколы работают иначе: класс соответствует протоколу, если реализует нужные методы. Без наследования, без зависимостей, без импорта протокола в модуле с классом. Вот классический ABC:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from abc import ABC, abstractmethod
 
class Serializable(ABC):
    @abstractmethod
    def to_json(self) -> str:
        pass
    
    @abstractmethod
    def from_json(self, data: str):
        pass
 
class User(Serializable):  # Явное наследование обязательно
    def __init__(self, name: str):
        self.name = name
    
    def to_json(self) -> str:
        return f'{{"name": "{self.name}"}}'
    
    def from_json(self, data: str):
        # Разбор JSON
        pass
Теперь та же идея через протокол:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from typing import Protocol
 
class Serializable(Protocol):
    def to_json(self) -> str: ...
    def from_json(self, data: str): ...
 
class User:  # Никакого наследования!
    def __init__(self, name: str):
        self.name = name
    
    def to_json(self) -> str:
        return f'{{"name": "{self.name}"}}'
    
    def from_json(self, data: str):
        pass
 
def save_to_db(obj: Serializable) -> None:
    # User подходит, потому что реализует нужные методы
    data = obj.to_json()
    # Сохраняем...
Разница принципиальная. ABC проверяет: "Ты унаследовался от меня?" Протокол спрашивает: "Ты умеешь то, что мне нужно?" Первое называется номинальной типизацией - важно имя (название класса в иерархии). Второе - структурная типизация - важна структура (наличие методов). Я долго работал с Java, где интерфейсы требуют явной реализации через implements. Это даёт гарантии, но создаёт связанность. Меняешь интерфейс - перекомпилируешь все классы. В Python протоколы решают эту проблему элегантнее. Библиотека определяет протокол, ты пишешь класс, и они работают вместе - не зная друг о друге.

Утиная типизация всегда была в Python. "Если что-то крякает как утка, плавает как утка - это утка". Только проверка происходила в рантайме:

Python
1
2
3
def process(obj):
    # Надеемся, что у obj есть метод handle
    obj.handle()  # Узнаем только здесь, есть он или нет
Протоколы делают утиную типизацию статической. mypy анализирует код и говорит: "У твоего объекта нет метода handle, исправь до запуска". Та же гибкость, но с гарантиями на этапе разработки.
Документирование через протоколы - отдельная тема. Раньше писали в докстрингах: "Ожидается объект с методами x, y, z". Кто это читает? Протокол - живая документация в коде:

Python
1
2
3
4
5
6
7
8
9
class Drawable(Protocol):
    """Объекты, которые можно нарисовать на холсте"""
    def draw(self, canvas: Canvas) -> None:
        """Отрисовка объекта на переданном холсте"""
        ...
    
    def get_bounds(self) -> tuple[int, int, int, int]:
        """Возвращает границы объекта: (x, y, width, height)"""
        ...
Теперь любой класс, реализующий эти методы, автоматически считается Drawable. IDE подсказывает, какие методы нужны. Статический анализатор проверяет сигнатуры. Новый разработчик смотрит на протокол и сразу понимает контракт. Три месяца назад интегрировал стороннюю библиотеку в проект. Она ожидала объекты с определёнными методами, но нигде это не описывалось. Час потратил на изучение исходников, чтобы понять, что именно нужно реализовать. Если бы автор использовал протоколы - прочитал бы за минуту.

ABC не исчезают. Когда нужно общее поведение, переиспользование кода, защита от дурака - они незаменимы. Но для определения контрактов взаимодействия, особенно между независимыми модулями, протоколы работают чище. Меньше зависимостей, проще тестирование, гибче архитектура.

Как из Python скрипта выполнить другой python скрипт?
Как из Python скрипта выполнить другой python скрипт? Если он находится в той же папке но нужно...

Почему синтаксис Python 2.* и Python 3.* так отличается?
Привет! Решил на досуге заняться изучением Python'a. Читаю книгу по второму питону, а пользуюсь...

Что лучше учить Python 2 или Python 3?
хочу начать учить питон но полазив в нете, частенько попадалась информация что вроде как 2 будет...

Python without python
Доброго времени суток! Хотел узнать, что делать с *.py файлом после того как готова программа,...


Анатомия протоколов



Нажмите на изображение для увеличения
Название: Протоколы в Python 2.jpg
Просмотров: 36
Размер:	93.4 Кб
ID:	11360

Протоколы живут в модуле typing, который стал стандартной частью Python начиная с версии 3.5. Базовый класс Protocol появился в 3.8, хотя до этого его можно было использовать через пакет typing_extensions. Импортируется просто:

Python
1
from typing import Protocol
Объявление протокола выглядит как обычный класс с единственным отличием - наследованием от Protocol. Внутри описываешь методы и атрибуты, которые должен реализовать класс для соответствия протоколу:

Python
1
2
3
4
5
6
7
8
9
10
from typing import Protocol
 
class Comparable(Protocol):
    def __lt__(self, other) -> bool:
        """Меньше чем"""
        ...
    
    def __le__(self, other) -> bool:
        """Меньше или равно"""
        ...
Многоточие в теле метода - не заглушка. Это синтаксически корректный способ показать, что реализация не требуется. Можно написать pass, но многоточие стало негласным стандартом для протоколов. Короче, понятнее.
Атрибуты описываются через аннотации типов без присваивания значений:

Python
1
2
3
4
5
6
7
class Identifiable(Protocol):
    id: int  # Атрибут, который должен существовать
    name: str
    
    def get_id(self) -> int:
        """Получить идентификатор"""
        ...
Теперь любой класс с атрибутами id, name и методом get_id автоматически соответствует протоколу Identifiable. Без наследования, без явных деклараций.
Проверка соответствия по умолчанию работает только статически - через mypy, pyright или другие анализаторы. В рантайме isinstance и issubclass с протоколами не работают:

Python
1
2
3
4
5
6
7
8
9
10
class User:
    def __init__(self, user_id: int, username: str):
        self.id = user_id
        self.name = username
    
    def get_id(self) -> int:
        return self.id
 
user = User(42, "admin")
# isinstance(user, Identifiable)  # Ошибка: Protocols cannot be used with isinstance
Год назад поймал баг именно на этом. Пытался использовать isinstance с протоколом для валидации входных данных в API. Падало с непонятной ошибкой. Полчаса искал причину, пока не вспомнил про декоратор runtime_checkable.
Чтобы включить проверки в рантайме, протокол нужно пометить декоратором:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from typing import Protocol, runtime_checkable
 
@runtime_checkable
class Identifiable(Protocol):
    id: int
    name: str
    
    def get_id(self) -> int: ...
 
# Теперь isinstance работает
user = User(42, "admin")
print(isinstance(user, Identifiable))  # True
Но тут важный нюанс. Проверка в рантайме смотрит только на наличие атрибутов и методов, но не проверяет их сигнатуры. Метод может принимать совершенно другие аргументы, возвращать не тот тип - isinstance всё равно вернёт True. Это поверхностная проверка, а не полная валидация контракта.

Python
1
2
3
4
5
6
7
8
9
10
@runtime_checkable
class Processor(Protocol):
    def process(self, data: str) -> dict: ...
 
class BrokenProcessor:
    def process(self, x: int, y: int) -> list:  # Другая сигнатура!
        return [x, y]
 
broken = BrokenProcessor()
print(isinstance(broken, Processor))  # True - только проверка наличия метода!
Этот момент часто упускают. runtime_checkable полезен для быстрых проверок на входе функции, но полагаться на него как на полноценную валидацию - ошибка. Статический анализ ловит такие несоответствия, рантайм-проверки - нет.
issubclass тоже работает с runtime_checkable протоколами:

Python
1
2
print(issubclass(User, Identifiable))  # True
print(issubclass(BrokenProcessor, Processor))  # True
Опять же, проверка поверхностная. Класс формально соответствует протоколу по наличию членов, но их типы и сигнатуры могут не совпадать.
Протоколы можно комбинировать через наследование. Один протокол расширяет другой:

Python
1
2
3
4
5
6
7
8
9
class Readable(Protocol):
    def read(self) -> bytes: ...
 
class Writable(Protocol):
    def write(self, data: bytes) -> int: ...
 
class ReadWritable(Readable, Writable, Protocol):
    """Объект, который можно читать и записывать"""
    pass
Класс, соответствующий ReadWritable, должен реализовать методы обоих родительских протоколов. Это композиция контрактов, и она работает интуитивно.
Протоколы могут содержать свойства через декоратор @property:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Sized(Protocol):
    @property
    def size(self) -> int:
        """Размер объекта"""
        ...
 
class Buffer:
    def __init__(self, capacity: int):
        self._capacity = capacity
    
    @property
    def size(self) -> int:
        return self._capacity
 
# Buffer соответствует Sized
Недавно писал систему кеширования, где протоколы с property оказались очень кстати. Разные реализации кеша (в памяти, Redis, файловый) имели одинаковый интерфейс через свойства, без лишних методов-геттеров.
Интересная деталь: протоколы могут содержать реализацию методов. Это не обязывает классы переопределять их, но даёт возможность предоставить дефолтное поведение:

Python
1
2
3
4
5
6
7
class Timestamped(Protocol):
    created_at: float
    
    def age(self) -> float:
        """Возвращает возраст объекта в секундах"""
        import time
        return time.time() - self.created_at
Класс с атрибутом created_at автоматически получает метод age. Если хочет переопределить - может, но не обязан. Правда, это скорее исключение. Обычно протоколы содержат только сигнатуры.
Generic протоколы с TypeVar позволяют параметризовать типы. Это мощный инструмент для универсальных контейнеров:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from typing import Protocol, TypeVar
 
T = TypeVar('T')
 
class Container(Protocol[T]):
    def add(self, item: T) -> None: ...
    def get(self) -> T: ...
 
class IntContainer:
    def __init__(self):
        self._items: list[int] = []
    
    def add(self, item: int) -> None:
        self._items.append(item)
    
    def get(self) -> int:
        return self._items[0] if self._items else 0
 
# IntContainer соответствует Container[int]
Обобщённые протоколы незаменимы при работе с коллекциями, фабриками, паттернами стратегии - везде, где тип данных может меняться, но интерфейс остаётся единым.
Протоколы не создают экземпляров. Попытка инстанцировать протокол вызовет TypeError:

Python
1
2
3
4
5
class Loggable(Protocol):
    def log(self, message: str) -> None: ...
 
# Так нельзя
# logger = Loggable()  # TypeError: Protocols cannot be instantiated
Это логично - протокол описывает контракт, а не реализацию. Он говорит "что должно быть", но не "как это работает". Для создания объектов нужны обычные классы.
Модуль typing предоставляет несколько готовых протоколов для стандартных операций. SupportsInt, SupportsFloat, SupportsComplex - описывают объекты, конвертируемые в соответствующие типы:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from typing import SupportsInt
 
def process_numeric(value: SupportsInt) -> int:
    # Принимаем всё, что можно превратить в int
    return int(value)
 
class Custom:
    def __int__(self) -> int:
        return 42
 
# Работает
result = process_numeric(Custom())  # 42
SupportsAbs, SupportsRound, SupportsBytes - аналогичные протоколы для других операций. Использую их постоянно в функциях, работающих с числами или строками, когда не хочу ограничиваться конкретными типами.
Протоколы могут содержать магические методы. Это открывает интересные возможности:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Addable(Protocol):
    def __add__(self, other) -> 'Addable':
        """Поддержка оператора +"""
        ...
 
class Vector2D:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y
    
    def __add__(self, other: 'Vector2D') -> 'Vector2D':
        return Vector2D(self.x + other.x, self.y + other.y)
 
def combine(a: Addable, b: Addable) -> Addable:
    return a + b
 
# Vector2D соответствует Addable
v1 = Vector2D(1, 2)
v2 = Vector2D(3, 4)
v3 = combine(v1, v2)  # Статический анализатор доволен
Месяц назад рефакторил математическую библиотеку для обработки геометрических примитивов. Протоколы с магическими методами позволили написать универсальные функции, работающие с точками, векторами, матрицами - без общего базового класса и без дублирования кода.
Вложенные протоколы тоже работают, хотя используются редко:

Python
1
2
3
4
5
6
class Serializer(Protocol):
    class Config(Protocol):
        indent: int
        sort_keys: bool
    
    def serialize(self, obj: object, config: Config) -> str: ...
Честно говоря, за пять лет ни разу не видел такое в production коде. Слишком сложно, проще разделить на независимые протоколы.
Ковариантность и контравариантность в протоколах работают через параметры TypeVar:

Python
1
2
3
4
5
6
7
8
9
10
from typing import Protocol, TypeVar
 
T_co = TypeVar('T_co', covariant=True)  # Ковариантный параметр
T_contra = TypeVar('T_contra', contravariant=True)  # Контравариантный
 
class Producer(Protocol[T_co]):
    def produce(self) -> T_co: ...
 
class Consumer(Protocol[T_contra]):
    def consume(self, item: T_contra) -> None: ...
Ковариантность означает, что Producer[Dog] является подтипом Producer[Animal]. Контравариантность - обратное: Consumer[Animal] является подтипом Consumer[Dog]. Это важно для корректной проверки типов в иерархиях, но в повседневной разработке использую нечасто. В основном при работе с коллекциями и callback-функциями.
Протоколы не поддерживают множественное наследование от обычных классов. Либо Protocol, либо обычный класс:

Python
1
2
3
4
5
6
class BaseClass:
    def method(self) -> None: ...
 
# Так нельзя
# class Mixed(BaseClass, Protocol):  # TypeError
#     def protocol_method(self) -> None: ...
Пытался обойти это ограничение пару раз - не получилось. Приходится выбирать: либо протокол, либо базовый класс с наследованием. Гибридные решения не работают, и это правильно - смешивание номинальной и структурной типизации привело бы к путанице.

Встроенные протоколы стандартной библиотеки



Python имел протоколы задолго до появления typing.Protocol. Магические методы всегда определяли поведение объектов через структурную типизацию. Разница лишь в том, что раньше это работало только в рантайме, а теперь можно проверять статически. Возьмём итерируемые объекты. Любой класс с методом __iter__ считается итерируемым, и его можно использовать в циклах:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Countdown:
    def __init__(self, start: int):
        self.current = start
    
    def __iter__(self):
        # Возвращаем итератор
        return CountdownIterator(self.current)
 
class CountdownIterator:
    def __init__(self, start: int):
        self.value = start
    
    def __iter__(self):
        return self
    
    def __next__(self) -> int:
        if self.value <= 0:
            raise StopIteration
        self.value -= 1
        return self.value + 1
 
# Работает в цикле
for num in Countdown(5):
    print(num)  # 5, 4, 3, 2, 1
Модуль collections.abc формализует это через ABC, но typing предоставляет протоколы. Iterable[T] описывает объект, возвращающий итератор:

Python
1
2
3
4
5
6
7
8
9
10
11
from typing import Iterator, Iterable
 
def process_items(items: Iterable[int]) -> None:
    for item in items:
        # Обработка каждого элемента
        print(item * 2)
 
# Подходит любой итерируемый объект
process_items([1, 2, 3])  # Список
process_items(range(5))   # Range
process_items(Countdown(3))  # Наш класс
Iterator[T] требует оба метода - и __iter__, и __next__. Важное различие: каждый итератор является итерируемым, но не наоборот. Список итерируем, но не итератор - он создаёт новый итератор при каждом вызове iter(). Полгода назад отлаживал генератор данных для обучения нейросети. Там были бесконечные потоки изображений, и я запутался в различиях между Iterable и Iterator. Функция ожидала Iterator, а я передавал класс с __iter__, который каждый раз создавал новый итератор. Результат - данные обрабатывались по кругу, обучение зависло. Явная аннотация типов через протоколы помогла найти проблему за минуты.

Контекстные менеджеры - ещё один фундаментальный протокол. Два магических метода: __enter__ и __exit__:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from typing import Protocol, TypeVar
 
T = TypeVar('T')
 
class ContextManager(Protocol[T]):
    def __enter__(self) -> T: ...
    def __exit__(self, exc_type, exc_val, exc_tb) -> bool | None: ...
 
class DatabaseConnection:
    def __init__(self, host: str):
        self.host = host
        self.connected = False
    
    def __enter__(self):
        # Устанавливаем соединение
        self.connected = True
        print(f"Подключились к {self.host}")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        # Закрываем соединение
        self.connected = False
        print(f"Отключились от {self.host}")
        return False  # Не подавляем исключения
 
def execute_query(manager: ContextManager[DatabaseConnection]) -> None:
    with manager as conn:
        # Работаем с подключением
        assert conn.connected
Использую контекстные менеджеры постоянно для управления ресурсами. Файлы, сетевые соединения, блокировки - всё что требует гарантированной очистки. Протокол ContextManager позволяет написать универсальные функции, работающие с любыми менеджерами без привязки к конкретным классам.
Callable - протокол для вызываемых объектов. Работает с функциями, методами, классами с __call__:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from typing import Callable
 
class Multiplier:
    def __init__(self, factor: int):
        self.factor = factor
    
    def __call__(self, x: int) -> int:
        # Объект можно вызывать как функцию
        return x * self.factor
 
def apply_operation(value: int, operation: Callable[[int], int]) -> int:
    return operation(value)
 
doubler = Multiplier(2)
result = apply_operation(10, doubler)  # 20
result = apply_operation(10, lambda x: x + 5)  # 15
Три года назад разрабатывал систему обработки событий. Обработчики могли быть функциями, методами, классами - любыми вызываемыми объектами. Callable[...] в аннотациях типов сделал код самодокументируемым и помог поймать несколько ошибок, где передавались невызываемые объекты.
Sized протокол требует метод __len__. Container проверяет __contains__ для оператора in:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from typing import Sized, Container
 
class CustomSet:
    def __init__(self, items: list[int]):
        self._items = set(items)
    
    def __len__(self) -> int:
        return len(self._items)
    
    def __contains__(self, item: int) -> bool:
        return item in self._items
 
def check_size(obj: Sized) -> bool:
    # Работает с любым объектом, у которого есть len()
    return len(obj) > 0
 
def check_membership(container: Container[int], value: int) -> bool:
    return value in container
 
custom = CustomSet([1, 2, 3])
print(check_size(custom))  # True
print(check_membership(custom, 2))  # True
Collection объединяет несколько протоколов: Sized, Iterable и Container. Это композиция, и она показывает силу протоколов - можно комбинировать контракты без создания сложных иерархий наследования.
Дескрипторы тоже имеют протокол, хотя используются реже:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Descriptor(Protocol):
    def __get__(self, instance, owner): ...
    def __set__(self, instance, value): ...
    def __delete__(self, instance): ...
 
class ValidatedAttribute:
    def __init__(self, min_value: int):
        self.min_value = min_value
        self.value = 0
    
    def __get__(self, instance, owner):
        return self.value
    
    def __set__(self, instance, value: int):
        if value < self.min_value:
            raise ValueError(f"Значение должно быть >= {self.min_value}")
        self.value = value
 
class Product:
    price = ValidatedAttribute(0)  # Цена не может быть отрицательной
Когда писал ORM для внутреннего проекта, дескрипторы с протоколами оказались незаменимы для реализации полей модели. Явный контракт через протокол упростил расширение системы новыми типами полей.
Протоколы отлично сочетаются с dataclass и attrs. Эти инструменты автоматически генерируют методы, которые могут соответствовать протоколам:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from dataclasses import dataclass
from typing import Protocol
 
class Comparable(Protocol):
    def __lt__(self, other) -> bool: ...
    def __le__(self, other) -> bool: ...
 
@dataclass(order=True)  # Генерирует методы сравнения
class Score:
    value: int
    name: str
 
def get_max(a: Comparable, b: Comparable) -> Comparable:
    return a if a > b else b
 
# Score автоматически соответствует Comparable
score1 = Score(100, "Alice")
score2 = Score(85, "Bob")
winner = get_max(score1, score2)  # score1
Параметр order=True в dataclass генерирует все методы сравнения, делая класс совместимым с протоколами вроде Comparable. Не нужно вручную реализовывать магические методы - декоратор делает всё за тебя, и протокол просто проверяет, что они есть. attrs работает похожим образом, но с большей гибкостью:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import attr
from typing import Protocol
 
class Hashable(Protocol):
    def __hash__(self) -> int: ...
    def __eq__(self, other) -> bool: ...
 
@attr.s(frozen=True, auto_attribs=True)  # frozen генерирует __hash__
class UserId:
    value: int
    
    def validate(self):
        if self.value <= 0:
            raise ValueError("ID должен быть положительным")
 
# UserId соответствует Hashable
ids = {UserId(1), UserId(2), UserId(1)}  # Работает как множество
Параметр frozen=True делает объект неизменяемым и автоматически добавляет __hash__, что превращает класс в полноценный хешируемый тип. Это критично для использования в словарях и множествах.
Reversible требует метод __reversed__ для обратной итерации:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from typing import Reversible, Iterator
 
class Playlist:
    def __init__(self, tracks: list[str]):
        self._tracks = tracks
    
    def __iter__(self) -> Iterator[str]:
        return iter(self._tracks)
    
    def __reversed__(self) -> Iterator[str]:
        # Итерация в обратном порядке
        return reversed(self._tracks)
 
def shuffle_backwards(items: Reversible[str]) -> None:
    for item in reversed(items):
        print(f"Проигрываем: {item}")
 
playlist = Playlist(["Intro", "Main", "Outro"])
shuffle_backwards(playlist)  # Outro, Main, Intro
Писал аудиоплеер два года назад - там Reversible пригодился для реализации истории воспроизведения. Не пришлось городить отдельные методы для прямого и обратного обхода, всё работало через стандартный протокол.
Mapping и MutableMapping - протоколы для словарей и словареподобных структур:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from typing import Mapping, MutableMapping, Iterator
 
class ConfigStore:
    def __init__(self):
        self._data: dict[str, str] = {}
    
    def __getitem__(self, key: str) -> str:
        return self._data[key]
    
    def __setitem__(self, key: str, value: str) -> None:
        # Валидация при записи
        if not key.isupper():
            raise KeyError("Ключи должны быть в верхнем регистре")
        self._data[key] = value
    
    def __delitem__(self, key: str) -> None:
        del self._data[key]
    
    def __iter__(self) -> Iterator[str]:
        return iter(self._data)
    
    def __len__(self) -> int:
        return len(self._data)
 
def read_config(config: Mapping[str, str]) -> str:
    # Только чтение, изменения невозможны
    return config.get("DATABASE_URL", "localhost")
 
def update_config(config: MutableMapping[str, str], key: str, value: str) -> None:
    # Можно изменять
    config[key] = value
 
store = ConfigStore()
update_config(store, "API_KEY", "secret123")  # Ошибка: ключ не в верхнем регистре
Различие между Mapping и MutableMapping принципиально. Первый гарантирует неизменность - функция не сможет модифицировать данные даже случайно. Второй даёт полный доступ. Использую эту дихотомию везде, где работаю с конфигурациями или shared state между модулями.
Sequence и MutableSequence описывают списки и подобные структуры с индексацией:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from typing import Sequence, MutableSequence
 
class RingBuffer:
    def __init__(self, capacity: int):
        self._capacity = capacity
        self._data: list[int] = []
    
    def __getitem__(self, index: int) -> int:
        # Циклическая индексация
        return self._data[index % len(self._data)]
    
    def __setitem__(self, index: int, value: int) -> None:
        self._data[index % len(self._data)] = value
    
    def __len__(self) -> int:
        return len(self._data)
    
    def append(self, value: int) -> None:
        if len(self._data) < self._capacity:
            self._data.append(value)
        else:
            # Перезаписываем старейший элемент
            self._data[len(self._data) % self._capacity] = value
 
def sum_sequence(seq: Sequence[int]) -> int:
    # Работает с любой последовательностью
    total = 0
    for val in seq:
        total += val
    return total
 
buffer = RingBuffer(5)
buffer.append(10)
buffer.append(20)
print(sum_sequence(buffer))  # 30
AsyncIterable и AsyncIterator - асинхронные аналоги обычных протоколов итерации. Требуют __aiter__ и __anext__:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from typing import AsyncIterator
import asyncio
 
class AsyncCounter:
    def __init__(self, limit: int):
        self.limit = limit
        self.current = 0
    
    def __aiter__(self):
        return self
    
    async def __anext__(self) -> int:
        if self.current >= self.limit:
            raise StopAsyncIteration
        # Имитация асинхронной операции
        await asyncio.sleep(0.1)
        self.current += 1
        return self.current
 
async def process_async(source: AsyncIterator[int]) -> None:
    async for value in source:
        print(f"Обработали: {value}")
 
# asyncio.run(process_async(AsyncCounter(3)))
Полгода назад переписывал парсер логов на асинхронный вариант. AsyncIterator позволил создать единый интерфейс для чтения из файлов, сетевых потоков и баз данных. Код получился чище, чем с callback-функциями или ручным управлением корутинами.
SupportsAbs, SupportsRound и подобные протоколы проверяют поддержку встроенных функций:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from typing import SupportsAbs, SupportsRound
 
class ComplexNumber:
    def __init__(self, real: float, imag: float):
        self.real = real
        self.imag = imag
    
    def __abs__(self) -> float:
        # Модуль комплексного числа
        return (self.real [B] 2 + self.imag [/B] 2) ** 0.5
    
    def __round__(self, ndigits: int = 0) -> 'ComplexNumber':
        return ComplexNumber(
            round(self.real, ndigits),
            round(self.imag, ndigits)
        )
 
def calculate_distance(point: SupportsAbs[float]) -> float:
    # abs() работает с любым объектом, поддерживающим __abs__
    return abs(point)
 
num = ComplexNumber(3.0, 4.0)
print(calculate_distance(num))  # 5.0
Эти протоколы кажутся мелочью, но избавляют от необходимости писать isinstance-проверки или try-except блоки. Статический анализатор видит, что объект поддерживает нужную операцию, и этого достаточно.

Создание собственных протоколов



Определить протокол просто - опиши методы и атрибуты, которые тебе нужны. Сложнее сделать это так, чтобы контракт был понятным, полезным и не превращался в монстра с двадцатью методами, из которых реально используется три.

Начну с реального примера. Год назад разрабатывал систему уведомлений для микросервисной архитектуры. Email, SMS, push-уведомления, Slack, Telegram - куча каналов. Первая мысль - создать базовый класс NotificationChannel. Но зачем? Каналы ничего общего между собой не имеют, кроме способности отправлять сообщения. Протокол решил проблему элегантно:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from typing import Protocol
 
class NotificationChannel(Protocol):
    def send(self, recipient: str, message: str) -> bool:
        """Отправка сообщения получателю. Возвращает True при успехе"""
        ...
    
    def is_available(self) -> bool:
        """Проверка доступности канала"""
        ...
 
class EmailNotifier:
    def __init__(self, smtp_host: str, smtp_port: int):
        self.host = smtp_host
        self.port = smtp_port
        self.connected = False
    
    def send(self, recipient: str, message: str) -> bool:
        # Отправка через SMTP
        if not self.connected:
            return False
        print(f"Email на {recipient}: {message}")
        return True
    
    def is_available(self) -> bool:
        return self.connected
 
class TelegramNotifier:
    def __init__(self, bot_token: str):
        self.token = bot_token
    
    def send(self, recipient: str, message: str) -> bool:
        # Отправка через Telegram API
        print(f"Telegram для {recipient}: {message}")
        return True
    
    def is_available(self) -> bool:
        return bool(self.token)
 
def broadcast(channels: list[NotificationChannel], users: list[str], text: str) -> None:
    """Рассылка сообщения через все доступные каналы"""
    for channel in channels:
        if not channel.is_available():
            continue
        for user in users:
            channel.send(user, text)
Никакого наследования, никаких зависимостей. EmailNotifier и TelegramNotifier даже не знают о существовании протокола. Они просто реализуют нужные методы, и этого достаточно. Добавить новый канал? Пиши класс с методами send и is_available - готово. Контракт должен быть минималистичным. Два-три метода максимум, редко больше. Если протокол требует десять методов - что-то не так с архитектурой. Либо ты пытаешься запихнуть слишком много ответственности в один контракт, либо нужно разбить на несколько специализированных протоколов.

Композиция протоколов - мощный инструмент. Вместо одного большого контракта создаёшь несколько маленьких и комбинируешь их:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from typing import Protocol
 
class Serializable(Protocol):
    def to_dict(self) -> dict:
        """Преобразование объекта в словарь"""
        ...
 
class Validatable(Protocol):
    def validate(self) -> bool:
        """Проверка корректности данных"""
        ...
 
class Persistable(Serializable, Validatable, Protocol):
    """Объект, который можно сериализовать, валидировать и сохранять"""
    def save_to_db(self) -> None:
        """Сохранение в базу данных"""
        ...
 
class UserProfile:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
    
    def to_dict(self) -> dict:
        return {"name": self.name, "age": self.age}
    
    def validate(self) -> bool:
        # Простая валидация
        return len(self.name) > 0 and self.age >= 0
    
    def save_to_db(self) -> None:
        if not self.validate():
            raise ValueError("Некорректные данные профиля")
        # Сохранение в базу
        data = self.to_dict()
        print(f"Сохраняем: {data}")
 
def persist_entity(entity: Persistable) -> None:
    """Универсальная функция сохранения"""
    if entity.validate():
        entity.save_to_db()
        print(f"Сохранено: {entity.to_dict()}")
    else:
        print("Валидация провалилась")
 
profile = UserProfile("Alice", 30)
persist_entity(profile)
Три простых протокола объединились в один сложный. Можно использовать их по отдельности или вместе. Функция может требовать только Serializable, если ей нужна сериализация. Или Persistable, если нужен полный цикл. Гибкость без раздувания иерархий наследования.
Обобщённые протоколы с TypeVar позволяют параметризовать типы. Незаменимо для универсальных контейнеров, фабрик, стратегий:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from typing import Protocol, TypeVar, Generic
 
T = TypeVar('T')
R = TypeVar('R')
 
class Mapper(Protocol[T, R]):
    """Преобразование объектов одного типа в другой"""
    def map(self, item: T) -> R:
        ...
 
class IntToStringMapper:
    def map(self, item: int) -> str:
        # Преобразуем число в строку с форматированием
        return f"Число: {item:05d}"
 
class UserToJsonMapper:
    def map(self, item: dict) -> str:
        # Преобразуем словарь в JSON-строку
        import json
        return json.dumps(item)
 
def transform_batch(items: list[T], mapper: Mapper[T, R]) -> list[R]:
    """Применение преобразования к списку элементов"""
    return [mapper.map(item) for item in items]
 
# Работает с разными типами
int_mapper = IntToStringMapper()
strings = transform_batch([1, 2, 3], int_mapper)  # ["Число: 00001", ...]
 
user_mapper = UserToJsonMapper()
users = [{"name": "Alice"}, {"name": "Bob"}]
jsons = transform_batch(users, user_mapper)  # ['{"name": "Alice"}', ...]
TypeVar делает протокол типобезопасным. Статический анализатор понимает, что если передаёшь список int и Mapper[int, str], то получишь list[str]. Без Generic пришлось бы использовать Any или писать отдельные протоколы для каждой комбинации типов.
Ковариантность и контравариантность добавляют гибкости, но усложняют понимание:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from typing import Protocol, TypeVar
 
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
 
class Source(Protocol[T_co]):
    """Источник данных - только чтение"""
    def get(self) -> T_co:
        ...
 
class Sink(Protocol[T_contra]):
    """Приёмник данных - только запись"""
    def put(self, item: T_contra) -> None:
        ...
 
class IntSource:
    def get(self) -> int:
        return 42
 
class NumberSink:
    def put(self, item: int | float) -> None:
        print(f"Получили число: {item}")
 
# Source ковариантен: Source[int] является подтипом Source[int | float]
# Sink контравариантен: Sink[int | float] является подтипом Sink[int]
Честно говоря, за три года активной разработки с протоколами использовал ковариантность от силы раз пять. В большинстве случаев инвариантные TypeVar работают нормально. Но когда пишешь библиотеку с публичным API - ковариантность критична для корректной проверки типов.
Протоколы могут содержать свойства через @property, и это открывает интересные возможности:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from typing import Protocol
 
class Measurable(Protocol):
    @property
    def width(self) -> float:
        """Ширина объекта"""
        ...
    
    @property
    def height(self) -> float:
        """Высота объекта"""
        ...
 
class Rectangle:
    def __init__(self, w: float, h: float):
        self._width = w
        self._height = h
    
    @property
    def width(self) -> float:
        return self._width
    
    @property
    def height(self) -> float:
        return self._height
 
class Circle:
    def __init__(self, radius: float):
        self._radius = radius
    
    @property
    def width(self) -> float:
        # Диаметр круга
        return self._radius * 2
    
    @property
    def height(self) -> float:
        return self._radius * 2
 
def calculate_area(obj: Measurable) -> float:
    """Приблизительная площадь"""
    return obj.width * obj.height
 
rect = Rectangle(10, 5)
circ = Circle(3)
print(calculate_area(rect))  # 50.0
print(calculate_area(circ))  # 36.0
Свойства в протоколах полезны, когда интерфейс должен скрывать внутреннюю реализацию. Объект может хранить данные как угодно - в атрибутах, вычислять на лету, получать из базы. Протокол требует только наличие readable property с определённым типом.
Протоколы могут требовать магические методы, что даёт контроль над операторами:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from typing import Protocol
 
class Comparable(Protocol):
    def __lt__(self, other) -> bool:
        """Оператор <"""
        ...
    
    def __le__(self, other) -> bool:
        """Оператор <="""
        ...
 
class Priority:
    def __init__(self, level: int, name: str):
        self.level = level
        self.name = name
    
    def __lt__(self, other: 'Priority') -> bool:
        # Сравнение по уровню приоритета
        return self.level < other.level
    
    def __le__(self, other: 'Priority') -> bool:
        return self.level <= other.level
 
def find_highest(items: list[Comparable]) -> Comparable:
    """Поиск элемента с максимальным приоритетом"""
    if not items:
        raise ValueError("Список пуст")
    highest = items[0]
    for item in items[1:]:
        if item > highest:  # Использует __lt__ и __le__
            highest = item
    return highest
 
tasks = [Priority(3, "Low"), Priority(1, "Critical"), Priority(2, "Medium")]
urgent = find_highest(tasks)
print(urgent.name)  # "Critical"
Протоколы с магическими методами позволяют писать алгоритмы, работающие с любыми сравнимыми объектами. Не нужно знать, что именно сравнивается - числа, строки, кастомные классы. Главное, что они поддерживают нужные операторы.
Вложенность и сложные структуры в протоколах возможны, но редко оправданы:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from typing import Protocol
 
class Repository(Protocol):
    def find(self, id: int) -> dict | None:
        """Поиск записи по ID"""
        ...
    
    def save(self, data: dict) -> int:
        """Сохранение и возврат ID"""
        ...
 
class UnitOfWork(Protocol):
    repository: Repository  # Протокол как атрибут!
    
    def commit(self) -> None:
        """Фиксация транзакции"""
        ...
    
    def rollback(self) -> None:
        """Откат изменений"""
        ...
Такие конструкции усложняют код без явной пользы. Если Repository - протокол, то UnitOfWork не может проверить его в рантайме без runtime_checkable. А статический анализ путается в сложных зависимостях. Лучше держать протоколы простыми и независимыми.

Протоколы и статический анализ кода



Нажмите на изображение для увеличения
Название: Протоколы в Python 3.jpg
Просмотров: 25
Размер:	119.2 Кб
ID:	11361

Статический анализ без протоколов проверяет типы поверхностно. Если функция ожидает BaseLogger, передать можно только его наследников. Протоколы снимают это ограничение - проверяется структура, а не родословная. mypy видит: есть нужные методы? Типы совпадают? Пропускает. Нет - ругается ещё до запуска программы.
Запускаю mypy на проекте обычно так:

Python
1
mypy --strict --show-error-codes src/
Режим --strict включает все проверки сразу. Жестоко, но эффективно. Находит проблемы, о которых даже не подозревал. Месяц назад добавил протокол для обработчиков событий в микросервисе. mypy сразу показал три класса, где методы возвращали не тот тип. В продакшене это вылезло бы через неделю в виде странных багов.
Возьмём простой пример - функция принимает логгер:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from typing import Protocol
 
class Logger(Protocol):
    def log(self, message: str, level: str) -> None:
        ...
 
def process_data(logger: Logger, data: list[int]) -> None:
    logger.log(f"Обработка {len(data)} элементов", "INFO")
    # Обработка данных
    logger.log("Готово", "INFO")
 
class FileLogger:
    def __init__(self, path: str):
        self.path = path
    
    def log(self, message: str) -> None:  # Неправильная сигнатура!
        # Записываем в файл
        with open(self.path, 'a') as f:
            f.write(message + '\n')
 
# mypy увидит ошибку
file_logger = FileLogger("app.log")
process_data(file_logger, [1, 2, 3])  # error: Missing positional argument "level"
mypy моментально ловит несоответствие. FileLogger.log принимает один аргумент, а протокол требует два. До запуска, без тестов, без дебаггинга. Исправляешь сигнатуру - и всё работает.
pyright работает похоже, но быстрее и интегрируется в VS Code нативно. Я переключился на него полгода назад - скорость анализа выше раза в три. На большом проекте разница ощутима. Конфигурация через pyrightconfig.json:

JSON
1
2
3
4
5
{
  "typeCheckingMode": "strict",
  "reportMissingTypeStubs": true,
  "pythonVersion": "3.11"
}
Обнаружение несоответствий происходит на уровне сигнатур методов, но не только. Протоколы проверяют наличие атрибутов, их типы, ковариантность возвращаемых значений:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from typing import Protocol
 
class Storage(Protocol):
    capacity: int  # Атрибут обязателен
    
    def store(self, item: str) -> bool:
        ...
    
    def retrieve(self) -> str | None:
        ...
 
class MemoryStorage:
    def __init__(self):
        self.data: list[str] = []
        # Забыли capacity!
    
    def store(self, item: str) -> bool:
        self.data.append(item)
        return True
    
    def retrieve(self) -> str | None:
        return self.data.pop() if self.data else None
 
def use_storage(storage: Storage) -> None:
    print(f"Ёмкость: {storage.capacity}")  # mypy: error: "MemoryStorage" has no attribute "capacity"
Статический анализатор видит, что MemoryStorage не соответствует Storage - нет атрибута capacity. Добавляешь self.capacity в __init__ - проблема решена. Без протокола эта ошибка всплыла бы в рантайме при обращении к атрибуту.

Постепенная типизация legacy кода - отдельная песня. Год назад взялся за проект на 50 тысяч строк без единой аннотации типов. Переписывать с нуля нереально, заказчик не даст времени. Протоколы спасли ситуацию. Стратегия простая: начинаешь с критичных модулей. Определяешь протоколы для публичных интерфейсов, добавляешь аннотации в сигнатуры функций. Внутренности классов пока игнорируешь - работаешь только с границами модулей:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
# Было
def send_notification(channel, user, message):
    channel.send(user, message)
 
# Стало
from typing import Protocol
 
class NotificationChannel(Protocol):
    def send(self, recipient: str, message: str) -> bool:
        ...
 
def send_notification(channel: NotificationChannel, user: str, message: str) -> None:
    channel.send(user, message)
Запускаешь mypy только на этом модуле, не трогая остальной код:

Python
1
mypy --no-strict-optional src/notifications.py
Флаг --no-strict-optional отключает часть строгих проверок - для legacy кода это критично. Иначе mypy взорвётся от количества ошибок. Постепенно ужесточаешь правила, переходя к соседним модулям.
За три месяца покрыл протоколами весь публичный API проекта. Внутренности остались нетипизированными, но интерфейсы стали надёжными. Баги на стыках модулей сократились на 60% - mypy ловил их до коммита.

Интеграция в CI/CD делает проверки автоматическими. В GitLab CI конфиг выглядит так:

YAML
1
2
3
4
5
6
7
8
type-check:
  stage: test
  image: python:3.11
  script:
    - pip install mypy
    - mypy --config-file mypy.ini src/
  only:
    - merge_requests
Каждый merge request проходит проверку типов. Ошибки - pipeline красный, код не мержится. Жестко, но дисциплинирует команду. Два месяца назад добавил это в проект - количество багов, связанных с несоответствием интерфейсов, упало до нуля.
mypy.ini настраиваешь под конкретный проект:

Python
1
2
3
4
5
6
7
8
9
10
11
[mypy]
python_version = 3.11
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = False  # Для legacy кода
 
[mypy-tests.*]
ignore_errors = True  # Игнорируем тесты пока
 
[mypy-third_party.*]
ignore_missing_imports = True  # Сторонние библиотеки без типов
Постепенно параметры ужесточаются. Сначала disallow_untyped_defs выключен, через полгода включаешь. Код обрастает типами естественным путём, без революций.
pre-commit хуки запускают mypy локально перед коммитом:

YAML
1
2
3
4
5
6
7
repos:
  - repo: [url]https://github.com/pre-commit/mirrors-mypy[/url]
    rev: v1.5.1
    hooks:
      - id: mypy
        args: [--strict]
        additional_dependencies: [types-requests]
Разработчик видит ошибки типов сразу, не дожидаясь CI. Удобно, хотя иногда бесит - хочешь быстро закоммитить мелкий фикс, а mypy требует добавить аннотации. Но оно того стоит.

Протоколы делают проверки типов гибкими. Можешь постепенно типизировать код, не ломая существующую функциональность. Начинаешь с интерфейсов, углубляешься по мере необходимости. Legacy проекты становятся типобезопасными без полной переписки, а новые получают надёжность из коробки. Разные анализаторы по-разному обрабатывают протоколы. mypy строже к Generic-параметрам, pyright лучше работает с переопределением методов. Pyre вообще экзотика - видел его только в проектах Facebook, и то давно. Большинство команд выбирают между mypy и pyright, остальное - нишевые инструменты.

Однажды столкнулся с забавной ситуацией: mypy пропускал код, pyright ругался. Протокол с Generic-параметром и сложная цепочка наследования. mypy упрощал проверку и считал всё нормальным, pyright копал глубже и находил несоответствие в ковариантности. Кто прав? Технически pyright, но в реальном коде это никогда не вызвало бы проблем. Пришлось добавить # type: ignore с комментарием для будущих поколений.

Generic протоколы требуют явного указания параметров типов при использовании. Забыл квадратные скобки - получишь ошибку:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from typing import Protocol, TypeVar
 
T = TypeVar('T')
 
class Container(Protocol[T]):
    def get(self) -> T: ...
 
def process(c: Container) -> None:  # Ошибка! Нужно Container[SomeType]
    pass
 
def process_correct(c: Container[int]) -> None:  # Правильно
    item = c.get()  # mypy знает, что item: int
Частая ошибка - использовать Any вместо TypeVar, думая что это одно и то же. Any отключает проверки типов полностью. TypeVar сохраняет информацию о типе и позволяет анализатору проверять согласованность.
Optional и Union в протоколах работают предсказуемо, но есть нюанс с возвращаемыми значениями. Если метод протокола возвращает str | None, реализация может вернуть только str - это валидно по принципу подстановки Барбары Лисков. Обратное неверно:

Python
1
2
3
4
5
6
7
8
9
10
class Reader(Protocol):
    def read(self) -> str | None: ...
 
class StrictReader:
    def read(self) -> str:  # Всегда возвращает строку - OK
        return "data"
 
class OptionalReader:
    def read(self) -> str | None:  # Может вернуть None - тоже OK
        return None if random.random() < 0.5 else "data"
Производительность статического анализа на больших проектах становится проблемой. mypy на 100 тысячах строк может работать минуту и больше. Решение - кеширование через --cache-dir и инкрементальный режим. После первого запуска последующие занимают секунды:

Python
1
mypy --cache-dir=.mypy_cache --incremental src/
pyright из коробки быстрее за счёт оптимизаций и параллельной обработки модулей. На том же проекте работает в три раза шустрее mypy. Но жрёт больше памяти - видел случаи, когда на CI с 2GB RAM падал с out of memory.

IDE интеграция делает протоколы удобнее. VS Code с Pylance (основан на pyright) подсвечивает ошибки типов в реальном времени, предлагает автодополнение по протоколам. PyCharm тоже научился работать с Protocol, хотя поддержка появилась позже и работает медленнее. Когда пишу класс, соответствующий протоколу, IDE сразу показывает, каких методов не хватает. Не нужно гадать - видишь список требований прямо в редакторе. Это экономит часы при работе со сложными интерфейсами.

Циклические импорты убивают проверку типов. Модуль A импортирует протокол из B, B импортирует класс из A - mypy путается и выдаёт странные ошибки. Решение через TYPE_CHECKING:

Python
1
2
3
4
5
6
7
from typing import TYPE_CHECKING, Protocol
 
if TYPE_CHECKING:
    from module_b import SomeClass  # Импорт только для проверки типов
 
class MyProtocol(Protocol):
    def method(self, obj: 'SomeClass') -> None: ...  # Строковая аннотация
Импорт внутри TYPE_CHECKING не выполняется в рантайме, разрывая цикл. Строковые аннотации откладывают разрешение типа до момента проверки. Грязный хак, но работает.

Реальные сценарии применения



Адаптеры к внешним API демонстрируют силу протоколов максимально наглядно. Полгода назад писал интеграцию с платёжными системами для e-commerce проекта. Stripe, PayPal, ЮMoney, Тинькофф - каждый со своим SDK, своими методами, своими моделями данных. Попытка создать единый базовый класс провалилась - слишком разные интерфейсы. Протокол решил проблему элегантно:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from typing import Protocol
from decimal import Decimal
 
class PaymentGateway(Protocol):
    def charge(self, amount: Decimal, currency: str, card_token: str) -> str:
        """Списание средств. Возвращает ID транзакции"""
        ...
    
    def refund(self, transaction_id: str, amount: Decimal) -> bool:
        """Возврат средств"""
        ...
    
    def get_status(self, transaction_id: str) -> str:
        """Проверка статуса платежа"""
        ...
 
class StripeAdapter:
    def __init__(self, api_key: str):
        import stripe
        stripe.api_key = api_key
        self.stripe = stripe
    
    def charge(self, amount: Decimal, currency: str, card_token: str) -> str:
        # Stripe работает с копейками
        amount_cents = int(amount * 100)
        charge = self.stripe.Charge.create(
            amount=amount_cents,
            currency=currency.lower(),
            source=card_token
        )
        return charge.id
    
    def refund(self, transaction_id: str, amount: Decimal) -> bool:
        try:
            refund = self.stripe.Refund.create(
                charge=transaction_id,
                amount=int(amount * 100)
            )
            return refund.status == 'succeeded'
        except Exception:
            return False
    
    def get_status(self, transaction_id: str) -> str:
        charge = self.stripe.Charge.retrieve(transaction_id)
        return charge.status
 
class TinkoffAdapter:
    def __init__(self, terminal_key: str, secret_key: str):
        self.terminal = terminal_key
        self.secret = secret_key
        self.base_url = "https://securepay.tinkoff.ru/v2/"
    
    def charge(self, amount: Decimal, currency: str, card_token: str) -> str:
        # Тинькофф работает с рублями в копейках
        import requests
        response = requests.post(
            f"{self.base_url}Init",
            json={
                "TerminalKey": self.terminal,
                "Amount": int(amount * 100),
                "OrderId": self._generate_order_id(),
                "DATA": {"Token": card_token}
            }
        )
        return response.json()["PaymentId"]
    
    def refund(self, transaction_id: str, amount: Decimal) -> bool:
        # Реализация возврата для Тинькофф
        return True
    
    def get_status(self, transaction_id: str) -> str:
        # Проверка статуса
        return "succeeded"
    
    def _generate_order_id(self) -> str:
        import uuid
        return str(uuid.uuid4())
Бизнес-логика работает с любым адаптером без изменений. Добавить новый шлюз? Пишешь класс с тремя методами - готово. Никаких изменений в существующем коде, никаких зависимостей между адаптерами.
Тестирование через моки становится тривиальным. Раньше мокал классы через unittest.mock.MagicMock или создавал фейковые реализации вручную. С протоколами моки пишутся за минуту:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MockPaymentGateway:
    def __init__(self):
        self.charges: dict[str, Decimal] = {}
        self.refunds: dict[str, Decimal] = {}
        self._counter = 0
    
    def charge(self, amount: Decimal, currency: str, card_token: str) -> str:
        self._counter += 1
        tx_id = f"mock_tx_{self._counter}"
        self.charges[tx_id] = amount
        return tx_id
    
    def refund(self, transaction_id: str, amount: Decimal) -> bool:
        if transaction_id in self.charges:
            self.refunds[transaction_id] = amount
            return True
        return False
    
    def get_status(self, transaction_id: str) -> str:
        return "succeeded" if transaction_id in self.charges else "failed"
 
def test_payment_flow():
    gateway: PaymentGateway = MockPaymentGateway()
    
    # Тестируем без реальных API-вызовов
    tx_id = gateway.charge(Decimal("100.50"), "RUB", "test_token")
    assert gateway.get_status(tx_id) == "succeeded"
    assert gateway.refund(tx_id, Decimal("50.00"))
Статический анализатор видит, что MockPaymentGateway соответствует протоколу. Тесты запускаются мгновенно, не бьют по внешним сервисам, не требуют настройки окружения. Написал полсотни таких тестов за вечер - раньше на это уходила неделя.
Система плагинов через протоколы работает без центрального реестра. Прошлым летом делал ETL-конвейер для обработки данных из разных источников. CSV, JSON, XML, базы данных, внешние API - форматов дюжина. Протокол DataSource описывал минимальный контракт:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from typing import Protocol, Iterator
 
class DataSource(Protocol):
    def fetch(self) -> Iterator[dict]:
        """Получение данных построчно"""
        ...
    
    def close(self) -> None:
        """Очистка ресурсов"""
        ...
 
class CSVSource:
    def __init__(self, filepath: str):
        self.filepath = filepath
        self.file = None
    
    def fetch(self) -> Iterator[dict]:
        import csv
        self.file = open(self.filepath, 'r', encoding='utf-8')
        reader = csv.DictReader(self.file)
        for row in reader:
            yield row
    
    def close(self) -> None:
        if self.file:
            self.file.close()
 
class PostgresSource:
    def __init__(self, connection_string: str):
        self.conn_string = connection_string
        self.connection = None
    
    def fetch(self) -> Iterator[dict]:
        import psycopg2
        import psycopg2.extras
        self.connection = psycopg2.connect(self.conn_string)
        cursor = self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor)
        cursor.execute("SELECT * FROM raw_data")
        for row in cursor:
            yield dict(row)
    
    def close(self) -> None:
        if self.connection:
            self.connection.close()
Конвейер обработки не знает об источниках данных ничего. Принимает DataSource, читает, трансформирует, загружает. Добавить новый источник? Класс с двумя методами, никаких регистраций, никаких изменений в пайплайне. За три месяца команда добавила восемь источников - каждый в отдельном модуле, без конфликтов.
Dependency Injection контейнеры обходятся без магии через протоколы. Год назад внедрял DI в монолитное приложение, где сервисы зависели друг от друга хаотично. Традиционный подход - фреймворк вроде dependency-injector с декораторами и регистрацией зависимостей. Я пошёл проще:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from typing import Protocol
 
class UserRepository(Protocol):
    def get_by_id(self, user_id: int) -> dict | None: ...
    def save(self, user: dict) -> None: ...
 
class EmailService(Protocol):
    def send(self, recipient: str, subject: str, body: str) -> None: ...
 
class UserService:
    def __init__(self, repo: UserRepository, email: EmailService):
        self.repo = repo
        self.email = email
    
    def register_user(self, username: str, email_addr: str) -> None:
        user = {"username": username, "email": email_addr}
        self.repo.save(user)
        self.email.send(
            email_addr,
            "Добро пожаловать!",
            f"Привет, {username}!"
        )
 
# Реализации можно менять без изменения UserService
class SQLRepository:
    def get_by_id(self, user_id: int) -> dict | None:
        # SQL запрос
        return {"id": user_id, "username": "test"}
    
    def save(self, user: dict) -> None:
        # INSERT запрос
        pass
 
class SMTPEmail:
    def send(self, recipient: str, subject: str, body: str) -> None:
        # Отправка через SMTP
        print(f"Email to {recipient}: {subject}")
Зависимости передаются явно через конструктор. Никаких глобальных синглтонов, никакой магии с импортами. Тесты пишутся элементарно - создал моки для протоколов, передал в конструктор, проверил логику. Рефакторинг сервисов перестал быть кошмаром.

Архитектурное разделение на слои через протоколы держит код в узде. Application layer определяет протоколы для инфраструктуры, но не зависит от реализаций. Domain layer вообще ничего не знает о базах, сети, файлах. Infrastructure layer реализует протоколы, зная о домене. Классическая чистая архитектура, только без абстрактных классов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# domain/models.py - чистая бизнес-логика
class Order:
    def __init__(self, order_id: int, items: list[dict]):
        self.id = order_id
        self.items = items
        self.total = sum(item['price'] for item in items)
    
    def apply_discount(self, percent: int) -> None:
        self.total *= (1 - percent / 100)
 
# application/protocols.py - контракты для инфраструктуры
from typing import Protocol
 
class OrderRepository(Protocol):
    def find(self, order_id: int) -> Order | None: ...
    def save(self, order: Order) -> None: ...
 
# application/services.py - бизнес-логика
class OrderService:
    def __init__(self, repo: OrderRepository):
        self.repo = repo
    
    def apply_promo_code(self, order_id: int, code: str) -> bool:
        order = self.repo.find(order_id)
        if not order:
            return False
        
        if code == "SALE20":
            order.apply_discount(20)
            self.repo.save(order)
            return True
        return False
 
# infrastructure/repositories.py - детали реализации
class PostgresOrderRepository:
    def __init__(self, db_url: str):
        self.db_url = db_url
    
    def find(self, order_id: int) -> Order | None:
        # SQL запрос и маппинг в Order
        return Order(order_id, [])
    
    def save(self, order: Order) -> None:
        # UPDATE в базе
        pass
Domain не импортирует ничего из infrastructure. Application слой знает только протоколы. Infrastructure реализует всё, что нужно. Меняешь базу с Postgres на MongoDB? Пишешь новый репозиторий, реализуешь протокол - готово. Остальной код даже не заметит.
Кеширование декораторами демонстрирует композируемость протоколов. Три недели назад оптимизировал API, где одни методы возвращали данные из памяти, другие из Redis, третьи вычисляли на лету. Протокол Cacheable дал единый интерфейс:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from typing import Protocol, TypeVar, Callable, Any
from functools import wraps
 
T = TypeVar('T')
 
class CacheBackend(Protocol):
  def get(self, key: str) -> Any | None:
      """Получение из кеша"""
      ...
  
  def set(self, key: str, value: Any, ttl: int) -> None:
      """Сохранение в кеш с TTL в секундах"""
      ...
 
class MemoryCache:
  def __init__(self):
      self._storage: dict[str, tuple[Any, float]] = {}
  
  def get(self, key: str) -> Any | None:
      import time
      if key in self._storage:
          value, expires = self._storage[key]
          if time.time() < expires:
              return value
          del self._storage[key]
      return None
  
  def set(self, key: str, value: Any, ttl: int) -> None:
      import time
      self._storage[key] = (value, time.time() + ttl)
 
def cached(backend: CacheBackend, ttl: int = 300):
  """Декоратор кеширования с произвольным бэкендом"""
  def decorator(func: Callable[..., T]) -> Callable[..., T]:
      @wraps(func)
      def wrapper(*args, **kwargs) -> T:
          # Генерируем ключ из аргументов
          cache_key = f"{func.__name__}:{args}:{kwargs}"
          
          # Проверяем кеш
          cached_value = backend.get(cache_key)
          if cached_value is not None:
              return cached_value
          
          # Вычисляем и кешируем
          result = func(*args, **kwargs)
          backend.set(cache_key, result, ttl)
          return result
      return wrapper
  return decorator
 
# Использование с любым бэкендом
memory = MemoryCache()
 
@cached(memory, ttl=60)
def expensive_calculation(n: int) -> int:
  # Тяжёлые вычисления
  return sum(i * i for i in range(n))
 
# Первый вызов вычисляет
result1 = expensive_calculation(10000)  # Медленно
 
# Второй берёт из кеша
result2 = expensive_calculation(10000)  # Мгновенно
Меняешь бэкенд на Redis - декоратор работает без изменений. Пишешь класс RedisCache с двумя методами - готово. Протокол изолирует логику кеширования от хранилища.
Валидация входных данных через протоколы убирает дублирование. В REST API обычно проверяешь типы, форматы, ограничения. Вместо копипасты валидаторов по контроллерам, пишешь протокол:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from typing import Protocol
 
class Validator(Protocol):
  def validate(self, value: Any) -> bool:
      """Проверка корректности значения"""
      ...
  
  def error_message(self) -> str:
      """Сообщение об ошибке"""
      ...
 
class EmailValidator:
  def validate(self, value: Any) -> bool:
      import re
      if not isinstance(value, str):
          return False
      pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
      return bool(re.match(pattern, value))
  
  def error_message(self) -> str:
      return "Некорректный формат email"
 
class RangeValidator:
  def __init__(self, min_val: int, max_val: int):
      self.min = min_val
      self.max = max_val
  
  def validate(self, value: Any) -> bool:
      if not isinstance(value, (int, float)):
          return False
      return self.min <= value <= self.max
  
  def error_message(self) -> str:
      return f"Значение должно быть от {self.min} до {self.max}"
 
def validate_field(value: Any, validators: list[Validator]) -> tuple[bool, list[str]]:
  """Проверка значения набором валидаторов"""
  errors = []
  for validator in validators:
      if not validator.validate(value):
          errors.append(validator.error_message())
  return len(errors) == 0, errors
 
# Композиция валидаторов
age_validators = [RangeValidator(0, 150)]
email_validators = [EmailValidator()]
 
is_valid, errors = validate_field(25, age_validators)  # (True, [])
is_valid, errors = validate_field("bad-email", email_validators)  # (False, [...])
Каждый валидатор независим, можно комбинировать произвольно. Добавить проверку уникальности в базе? Класс UniqueValidator реализует протокол - вписывается в существующую систему без переделки.
Стратегии обработки ошибок выигрывают от протоколов. В микросервисе было пять способов реагировать на сбои: retry, circuit breaker, fallback к дефолтным данным, логирование и игнорирование, немедленный fail. Протокол ErrorHandler унифицировал подход:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from typing import Protocol, Callable, TypeVar
 
T = TypeVar('T')
 
class ErrorHandler(Protocol[T]):
  def handle(self, error: Exception, operation: Callable[[], T]) -> T:
      """Обработка ошибки и возврат результата"""
      ...
 
class RetryHandler:
  def __init__(self, attempts: int = 3):
      self.attempts = attempts
  
  def handle(self, error: Exception, operation: Callable[[], T]) -> T:
      for attempt in range(self.attempts):
          try:
              return operation()
          except Exception as e:
              if attempt == self.attempts - 1:
                  raise e
              # Повторяем попытку
      raise error
 
class FallbackHandler:
  def __init__(self, default_value: T):
      self.default = default_value
  
  def handle(self, error: Exception, operation: Callable[[], T]) -> T:
      try:
          return operation()
      except Exception:
          return self.default
 
def execute_with_handler(operation: Callable[[], T], handler: ErrorHandler[T]) -> T:
  """Выполнение операции с заданным обработчиком ошибок"""
  try:
      return operation()
  except Exception as e:
      return handler.handle(e, operation)
 
# Разные стратегии для разных операций
retry = RetryHandler(attempts=5)
fallback = FallbackHandler(default_value=[])
 
# Критичная операция - ретраи
result = execute_with_handler(lambda: fetch_user_data(), retry)
 
# Некритичная - фолбек к пустому списку
items = execute_with_handler(lambda: load_recommendations(), fallback)
Выбор стратегии обработки ошибок стал конфигурационным решением, а не хардкодом в каждой функции. Меняется требование - меняешь handler, логика остаётся нетронутой.

Ловушки и подводные камни



Производительность runtime_checkable - первая засада, в которую попадают почти все. Декоратор включает проверки через isinstance, но работает он примитивно. Python просто смотрит: есть атрибут с таким именем? Есть метод? Отлично, объект соответствует протоколу. Типы не проверяются, сигнатуры не сравниваются, ничего глубокого. Полгода назад ловил баг в продакшене три дня. API принимал объекты, валидируя их через isinstance с протоколом. Всё работало в тестах, на стейдже - красота. На проде начались странные падения: метод вызывался с неправильными аргументами, возвращал не тот тип. Оказалось, один из модулей реализовал метод с другой сигнатурой - принимал два аргумента вместо одного, возвращал список вместо словаря. isinstance вернул True, потому что метод с таким именем существовал. Статический анализ в этом модуле был отключен - legacy код, некогда было типизировать.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from typing import Protocol, runtime_checkable
 
@runtime_checkable
class DataProcessor(Protocol):
    def process(self, data: dict) -> list[str]:
        """Обработка данных"""
        ...
 
class BrokenProcessor:
    def process(self, data: dict, options: dict) -> dict:  # Другая сигнатура!
        """Реализация с дополнительным параметром"""
        return {"result": "broken"}
 
# isinstance говорит True - метод process есть
broken = BrokenProcessor()
print(isinstance(broken, DataProcessor))  # True, но это ложь!
 
# А вот тут упадёт
def use_processor(proc: DataProcessor):
    result = proc.process({"key": "value"})  # TypeError: не хватает аргумента options!
    return result
Решение простое - не полагайся на runtime_checkable для критичных проверок. Используй его для быстрых санити-чеков на входе, не более. Настоящая валидация - через статический анализ и тесты. mypy поймает несоответствие сигнатур до запуска, isinstance - нет.

Еще одна засада - производительность самих проверок. isinstance с протоколом медленнее обычного isinstance примерно в десять раз. Python обходит все атрибуты класса, проверяет их типы через getattr, строит граф зависимостей. На одной проверке разница незаметна, но когда делаешь тысячи проверок в цикле - профайлер показывает isinstance в топе по времени.
Конфликты с наследованием возникают, когда пытаешься совместить протоколы и классическую иерархию. Вроде всё логично: класс наследуется от базового класса и одновременно соответствует протоколу. Но Python путается в приоритетах:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from typing import Protocol
 
class BaseService:
    def __init__(self):
        self.initialized = False
    
    def setup(self) -> None:
        """Инициализация сервиса"""
        self.initialized = True
 
class Loggable(Protocol):
    def log(self, message: str) -> None:
        """Логирование"""
        ...
 
class UserService(BaseService):
    def log(self, message: str) -> None:
        """Добавляем логирование"""
        print(f"[USER] {message}")
    
    def create_user(self, name: str) -> None:
        if not self.initialized:
            self.setup()
        self.log(f"Создаём пользователя {name}")
Всё нормально до тех пор, пока не начинаешь миксовать это с Generic протоколами или множественным наследованием от нескольких протоколов. Месяц назад столкнулся с ситуацией: класс наследовался от BaseRepository и соответствовал трём протоколам - Cacheable, Loggable, Measurable. mypy начал ругаться на несовместимость TypeVar между протоколами. Пришлось переделывать архитектуру, убирать наследование от BaseRepository, оставить только протоколы.
Структурная типизация проверяет форму, но не логику. Класс может иметь все нужные методы с правильными сигнатурами, но при этом работать совершенно неправильно. Протокол гарантирует только наличие интерфейса, не корректность поведения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from typing import Protocol
 
class Stack(Protocol):
    def push(self, item: int) -> None:
        """Добавление элемента"""
        ...
    
    def pop(self) -> int:
        """Извлечение элемента"""
        ...
 
class BrokenStack:
    def __init__(self):
        self.items: list[int] = []
    
    def push(self, item: int) -> None:
        # Реализация неправильная - добавляем в начало вместо конца!
        self.items.insert(0, item)
    
    def pop(self) -> int:
        # Извлекаем с конца
        return self.items.pop() if self.items else 0
 
# Статический анализ доволен - методы есть, типы совпадают
def use_stack(s: Stack) -> None:
    s.push(1)
    s.push(2)
    s.push(3)
    result = s.pop()  # Ожидаем 3, получим 1!
    print(result)
Протокол не гарантирует семантику операций. Он говорит "есть методы push и pop", но не проверяет, что push добавляет в конец, а pop извлекает оттуда же. Это ограничение структурной типизации. Тесты обязательны, без них протоколы дают ложное чувство безопасности.
Асинхронные протоколы добавляют путаницы. AsyncIterator выглядит как Iterator, но методы другие - __aiter__ вместо __iter__, __anext__ вместо __next__. Легко ошибиться и реализовать синхронную версию, думая что делаешь асинхронную:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from typing import AsyncIterator
import asyncio
 
class AsyncDataStream:
    def __init__(self, data: list[int]):
        self.data = data
        self.index = 0
    
    # Забыли сделать методы асинхронными!
    def __aiter__(self):
        return self
    
    def __anext__(self) -> int:  # Должно быть async def!
        if self.index >= len(self.data):
            raise StopAsyncIteration
        value = self.data[self.index]
        self.index += 1
        return value
 
async def process_stream(stream: AsyncIterator[int]) -> None:
    # Упадёт с TypeError - __anext__ должен возвращать awaitable
    async for item in stream:
        print(item)
Ошибка не очевидна при беглом взгляде на код. mypy может не заметить проблему, если аннотации типов стоят неправильно. Падение происходит в рантайме с загадочной ошибкой про awaitable. Два месяца назад потратил час на отладку похожего бага - забыл async перед def в одном из методов генератора событий.
Протоколы с Generic параметрами и вложенными структурами превращаются в ад для понимания. Когда TypeVar используется ковариантно, контравариантно и инвариантно в разных местах одного протокола - даже опытный разработчик путается:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from typing import Protocol, TypeVar, Generic
 
T_in = TypeVar('T_in', contravariant=True)
T_out = TypeVar('T_out', covariant=True)
 
class Transformer(Protocol[T_in, T_out]):
    def transform(self, input_data: T_in) -> T_out:
        """Преобразование входных данных в выходные"""
        ...
 
# Попробуй быстро понять, какие типы куда подставляются
class StringToIntTransformer:
    def transform(self, input_data: str) -> int:
        return len(input_data)
 
# Это Transformer[str, int] или наоборот? А может Transformer[int, str]?
# Контравариантность и ковариантность меняют правила игры
Читаемость кода страдает. Коллеги при ревью задают вопросы, почему TypeVar именно ковариантный. Приходится объяснять теорию типов вместо обсуждения бизнес-логики. Большинство проектов обходится инвариантными TypeVar - проще понимать, меньше ошибок.

Неявное несоответствие протоколу ловится только статическим анализом. Если его нет - узнаешь о проблеме в продакшене. Три недели назад видел код, где класс почти соответствовал протоколу. Метод был, но принимал на один аргумент больше с дефолтным значением. Технически работало, но mypy ругался. Команда игнорировала предупреждения, добавляя # type: ignore. Когда протокол изменился, добавился еще один обязательный аргумент - всё сломалось в продакшене. Пришлось экстренно фиксить, откатывать деплой.

Протоколы дают ложное ощущение гибкости. Кажется: вот оно, решение всех проблем с зависимостями. На практике излишнее увлечение протоколами раздувает кодовую базу. Начинаешь создавать протоколы для всего подряд - каждому классу свой протокол, для каждой функции отдельный контракт. Читать такой код невозможно, постоянно прыгаешь между файлами, разбираясь где реализация, где протокол, кто кому соответствует. Я видел проект, где на 10 тысяч строк кода было 200 протоколов. Треть вообще не использовалась, просто висела мертвым грузом. Половина содержала один метод - переделка их в обычные функции упростила бы код вдвое. Протоколы - инструмент, а не религия. Используй там, где действительно нужна гибкость и независимость компонентов, не везде подряд.

Версионирование протоколов превращается в головную боль при развитии API. Добавил новый метод в протокол - все старые реализации перестали соответствовать. mypy начинает ругаться на весь проект. Удалил метод - код компилируется, но падает в рантайме у тех, кто его вызывал. Изменил сигнатуру - статический анализатор кричит, но не всегда понятно где именно проблема.

Прошлой осенью поддерживал библиотеку с публичным протоколом Plugin. Добавили обязательный метод get_version() в новой версии - пользователи взвыли. Их плагины перестали работать, нужно было срочно обновлять код. Решили делать метод опциональным через hasattr проверку, но это убивает всю идею статической типизации. В итоге создали Plugin2 протокол с новым методом, оставив старый для совместимости. Теперь в коде два протокола, функции принимают Union[Plugin, Plugin2], читаемость упала.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from typing import Protocol, Union
 
class Plugin(Protocol):
    def execute(self) -> None: ...
 
class Plugin2(Protocol):
    def execute(self) -> None: ...
    def get_version(self) -> str: ...
 
def run_plugin(plugin: Union[Plugin, Plugin2]) -> None:
    plugin.execute()
    # Проверяем наличие метода в рантайме - некрасиво
    if hasattr(plugin, 'get_version'):
        print(f"Версия: {plugin.get_version()}")
Правильного решения нет. Либо ломаешь обратную совместимость, либо городишь костыли с проверками в рантайме. Семантическое версионирование помогает слабо - протокол это интерфейс, а не реализация. Любое изменение потенциально breaking change. IDE поддержка протоколов отстаёт от возможностей языка. VS Code с Pylance работает нормально, PyCharm тормозит и иногда не видит соответствия между классом и протоколом. Автодополнение глючит - предлагает методы, которых нет, или наоборот не показывает существующие. Рефакторинг через IDE опасен - переименовываешь метод в протоколе, а IDE не обновляет все реализации.

Monkey patching убивает гарантии протоколов. Python динамический язык, можно добавить методы объекту в рантайме. Статический анализ об этом не знает, проверяет исходный код класса. Результат - isinstance возвращает True, mypy думает что всё плохо:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from typing import Protocol, runtime_checkable
 
@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...
 
class Shape:
    pass
 
# Добавляем метод в рантайме
shape = Shape()
shape.draw = lambda: print("Рисуем фигуру")  # type: ignore
 
print(isinstance(shape, Drawable))  # True - метод есть
# Но mypy считает Shape несоответствующим Drawable
В тестах иногда делаешь такие трюки для быстрых моков. Работает, но анализатор ругается. Приходится расставлять type: ignore или писать полноценные mock-классы. Статическая типизация и динамические фичи Python конфликтуют.

Третьесторонние библиотеки без type hints превращают протоколы в декорацию. Пытаешься использовать объект из библиотеки, соответствующий твоему протоколу - mypy не видит методов, потому что у библиотеки нет аннотаций. Либо пишешь stub-файлы с типами вручную, либо оборачиваешь объекты в адаптеры. Оба варианта - дополнительная работа и код. Протоколы хороши, когда используются разумно. Один-два ключевых контракта на проект, четкие границы между модулями, осознанный выбор между протоколами и ABC. Но увлекаться не стоит - иначе получишь больше проблем, чем пользы.

Демонстрационный фреймворк для плагинов с протоколами



Полгода назад делал систему для обработки данных из разных источников. Требовалось добавлять новые форматы без перекомпиляции основного приложения. Классический случай для плагинов. Попробовал несколько подходов - через точки входа setuptools, через декораторы регистрации, через конфигурационные файлы. Везде был компромисс между гибкостью и типобезопасностью. Протоколы решили задачу элегантно.

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

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from typing import Protocol, Any
 
class DataPlugin(Protocol):
    """Контракт для плагинов обработки данных"""
    
    @property
    def name(self) -> str:
        """Уникальное имя плагина"""
        ...
    
    @property
    def version(self) -> str:
        """Версия плагина"""
        ...
    
    def initialize(self, config: dict[str, Any]) -> None:
        """Инициализация плагина с конфигурацией"""
        ...
    
    def process(self, data: bytes) -> dict[str, Any]:
        """Обработка входных данных"""
        ...
    
    def can_handle(self, data_type: str) -> bool:
        """Проверка поддержки типа данных"""
        ...
Архитектура держится на менеджере плагинов, который загружает модули, проверяет соответствие протоколу и маршрутизирует данные:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import importlib
import importlib.util
from pathlib import Path
from typing import runtime_checkable
 
@runtime_checkable
class DataPlugin(Protocol):
    # ... определение из примера выше
 
class PluginManager:
    """Менеджер загрузки и управления плагинами"""
    
    def __init__(self, plugins_dir: Path):
        self.plugins_dir = plugins_dir
        self.plugins: dict[str, DataPlugin] = {}
        self._load_errors: list[tuple[str, Exception]] = []
    
    def discover_plugins(self) -> None:
        """Поиск и загрузка всех плагинов из директории"""
        if not self.plugins_dir.exists():
            raise ValueError(f"Директория плагинов не существует: {self.plugins_dir}")
        
        # Ищем Python-модули
        for plugin_file in self.plugins_dir.glob("*.py"):
            if plugin_file.name.startswith("_"):
                continue  # Пропускаем служебные файлы
            
            try:
                self._load_plugin(plugin_file)
            except Exception as e:
                # Собираем ошибки, но не падаем
                self._load_errors.append((plugin_file.name, e))
    
    def _load_plugin(self, plugin_path: Path) -> None:
        """Загрузка одного плагина из файла"""
        module_name = plugin_path.stem
        
        # Динамический импорт модуля
        spec = importlib.util.spec_from_file_location(module_name, plugin_path)
        if spec is None or spec.loader is None:
            raise ImportError(f"Не удалось загрузить спецификацию для {plugin_path}")
        
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        
        # Ищем классы, соответствующие протоколу
        for attr_name in dir(module):
            if attr_name.startswith("_"):
                continue
            
            attr = getattr(module, attr_name)
            
            # Проверяем, что это класс и соответствует протоколу
            if not isinstance(attr, type):
                continue
            
            # Создаем экземпляр и проверяем через isinstance
            try:
                instance = attr()
                if isinstance(instance, DataPlugin):
                    plugin_name = instance.name
                    if plugin_name in self.plugins:
                        print(f"Предупреждение: плагин {plugin_name} уже загружен, пропускаем")
                        continue
                    
                    self.plugins[plugin_name] = instance
                    print(f"Загружен плагин: {plugin_name} v{instance.version}")
            except Exception as e:
                # Не смогли создать экземпляр - пропускаем
                print(f"Ошибка при инстанцировании {attr_name}: {e}")
    
    def get_plugin_for_type(self, data_type: str) -> DataPlugin | None:
        """Поиск плагина, поддерживающего указанный тип данных"""
        for plugin in self.plugins.values():
            if plugin.can_handle(data_type):
                return plugin
        return None
    
    def process_data(self, data: bytes, data_type: str, config: dict[str, Any] | None = None) -> dict[str, Any]:
        """Обработка данных через подходящий плагин"""
        plugin = self.get_plugin_for_type(data_type)
        if plugin is None:
            raise ValueError(f"Нет плагина для обработки типа: {data_type}")
        
        # Инициализируем с конфигурацией если нужно
        if config:
            plugin.initialize(config)
        
        return plugin.process(data)
    
    @property
    def load_errors(self) -> list[tuple[str, Exception]]:
        """Список ошибок при загрузке плагинов"""
        return self._load_errors
Теперь несколько реальных плагинов для демонстрации. JSON-процессор:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import json
from typing import Any
 
class JSONPlugin:
    """Плагин для обработки JSON-данных"""
    
    def __init__(self):
        self._config: dict[str, Any] = {}
    
    @property
    def name(self) -> str:
        return "json_processor"
    
    @property
    def version(self) -> str:
        return "1.0.0"
    
    def initialize(self, config: dict[str, Any]) -> None:
        """Сохраняем конфигурацию"""
        self._config = config
    
    def process(self, data: bytes) -> dict[str, Any]:
        """Парсинг JSON и добавление метаданных"""
        parsed = json.loads(data.decode('utf-8'))
        
        # Добавляем метаданные из конфигурации
        result = {
            "data": parsed,
            "plugin": self.name,
            "version": self.version
        }
        
        if "add_timestamp" in self._config and self._config["add_timestamp"]:
            import time
            result["processed_at"] = time.time()
        
        return result
    
    def can_handle(self, data_type: str) -> bool:
        """Поддерживаем JSON"""
        return data_type.lower() in ("json", "application/json")
XML-процессор демонстрирует обработку ошибок:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import xml.etree.ElementTree as ET
from typing import Any
 
class XMLPlugin:
    """Плагин для обработки XML-данных"""
    
    @property
    def name(self) -> str:
        return "xml_processor"
    
    @property
    def version(self) -> str:
        return "1.0.0"
    
    def initialize(self, config: dict[str, Any]) -> None:
        """XML-плагину конфигурация не нужна"""
        pass
    
    def process(self, data: bytes) -> dict[str, Any]:
        """Парсинг XML в словарь"""
        try:
            root = ET.fromstring(data.decode('utf-8'))
            
            # Преобразуем XML в словарь
            result = {
                "root_tag": root.tag,
                "attributes": root.attrib,
                "children": []
            }
            
            for child in root:
                result["children"].append({
                    "tag": child.tag,
                    "text": child.text,
                    "attributes": child.attrib
                })
            
            return result
            
        except ET.ParseError as e:
            # Возвращаем информацию об ошибке
            return {
                "error": "XML parsing failed",
                "message": str(e),
                "plugin": self.name
            }
    
    def can_handle(self, data_type: str) -> bool:
        """Поддерживаем XML"""
        return data_type.lower() in ("xml", "application/xml", "text/xml")
Валидация работает на двух уровнях. Статический анализ через mypy проверяет соответствие протоколу до запуска. Создаём тестовый скрипт для проверки типов:

Python
1
2
3
4
5
6
7
8
9
10
from typing import TYPE_CHECKING
 
if TYPE_CHECKING:
    # Импорты только для проверки типов
    from json_plugin import JSONPlugin
    from xml_plugin import XMLPlugin
    
    # mypy проверит, что классы соответствуют протоколу
    json_plugin: DataPlugin = JSONPlugin()
    xml_plugin: DataPlugin = XMLPlugin()
Запускаем mypy - если плагин не соответствует протоколу, увидим ошибку сразу. Это ловит проблемы на этапе разработки, до развёртывания. Runtime-валидация через isinstance отсекает плагины с несоответствующей структурой. Менеджер проверяет каждый загруженный класс и пропускает только те, что прошли проверку. Неправильные плагины попадают в список ошибок, но не роняют всю систему.
Использование фреймворка простое:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pathlib import Path
 
# Инициализация менеджера
manager = PluginManager(Path("./plugins"))
manager.discover_plugins()
 
# Проверка ошибок загрузки
if manager.load_errors:
    print("Ошибки при загрузке плагинов:")
    for filename, error in manager.load_errors:
        print(f"  {filename}: {error}")
 
# Обработка JSON-данных
json_data = b'{"name": "test", "value": 42}'
result = manager.process_data(
    json_data,
    "json",
    config={"add_timestamp": True}
)
print(f"JSON результат: {result}")
 
# Обработка XML-данных
xml_data = b'<root><item>test</item></root>'
result = manager.process_data(xml_data, "xml")
print(f"XML результат: {result}")
Расширение системы требует только создания нового файла в директории plugins. Класс реализует протокол - фреймворк подхватит автоматически. Никаких регистраций, никаких изменений в основном коде. Статический анализ гарантирует корректность интерфейса, runtime-проверки отсекают некорректные реализации.
Я использую похожую архитектуру в трёх продакшен-проектах. Один обрабатывает логи разных форматов, второй интегрируется с API внешних сервисов, третий реализует стратегии кеширования. Везде одна идея - протоколы определяют контракты, динамическая загрузка даёт гибкость, статический анализ обеспечивает надёжность.
Обработка приоритетов плагинов добавляет контроля, когда несколько обработчиков поддерживают один тип данных. Расширяю протокол дополнительным свойством:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from typing import Protocol
 
class PrioritizedPlugin(Protocol):
  """Протокол с поддержкой приоритетов"""
  
  name: str
  version: str
  
  @property
  def priority(self) -> int:
      """Приоритет плагина (больше = выше)"""
      ...
  
  def initialize(self, config: dict[str, Any]) -> None: ...
  def process(self, data: bytes) -> dict[str, Any]: ...
  def can_handle(self, data_type: str) -> bool: ...
Менеджер сортирует плагины по приоритету при выборе обработчика:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_plugin_for_type(self, data_type: str) -> DataPlugin | None:
  """Поиск плагина с наивысшим приоритетом"""
  candidates = [
      plugin for plugin in self.plugins.values()
      if plugin.can_handle(data_type)
  ]
  
  if not candidates:
      return None
  
  # Сортируем по приоритету, если он есть
  def get_priority(plugin: DataPlugin) -> int:
      return getattr(plugin, 'priority', 0)
  
  return max(candidates, key=get_priority)
Прошлым летом такая система спасла проект. Базовый CSV-плагин обрабатывал стандартные файлы с приоритетом 10. Добавили специализированный плагин для банковских выписок - приоритет 50. Он перехватывал обработку только для своего формата, остальное шло через базовый. Никакого if-else ада в роутинге.
Dependency injection через протоколы решает проблему общих ресурсов. Логгер, подключение к базе, кеш - всё это плагины могут получать через инициализацию:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Logger(Protocol):
  def log(self, level: str, message: str) -> None: ...
 
class PluginContext:
  """Контекст выполнения для плагинов"""
  
  def __init__(self, logger: Logger):
      self.logger = logger
      self.shared_cache: dict[str, Any] = {}
 
class ContextAwarePlugin(Protocol):
  """Плагин с доступом к контексту"""
  
  name: str
  version: str
  
  def set_context(self, context: PluginContext) -> None:
      """Установка контекста перед использованием"""
      ...
  
  def process(self, data: bytes) -> dict[str, Any]: ...
  def can_handle(self, data_type: str) -> bool: ...
Менеджер передаёт контекст при загрузке:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AdvancedPluginManager:
  def __init__(self, plugins_dir: Path, logger: Logger):
      self.plugins_dir = plugins_dir
      self.context = PluginContext(logger)
      self.plugins: dict[str, ContextAwarePlugin] = {}
  
  def _load_plugin(self, plugin_path: Path) -> None:
      # ... загрузка модуля
      
      instance = attr()
      
      # Передаём контекст если плагин его поддерживает
      if hasattr(instance, 'set_context'):
          instance.set_context(self.context)
      
      self.plugins[instance.name] = instance
Graceful degradation защищает от падений отдельных плагинов. Обёртка-декоратор перехватывает исключения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from functools import wraps
from typing import Callable, TypeVar
 
T = TypeVar('T')
 
def safe_plugin_call(fallback_value: T) -> Callable:
  """Декоратор для безопасного вызова методов плагина"""
  def decorator(func: Callable[..., T]) -> Callable[..., T]:
      @wraps(func)
      def wrapper(*args, **kwargs) -> T:
          try:
              return func(*args, **kwargs)
          except Exception as e:
              # Логируем ошибку
              print(f"Ошибка в плагине: {e}")
              return fallback_value
      return wrapper
  return decorator
 
class RobustPluginWrapper:
  """Обёртка плагина с защитой от ошибок"""
  
  def __init__(self, plugin: DataPlugin):
      self._plugin = plugin
  
  @property
  def name(self) -> str:
      return self._plugin.name
  
  @safe_plugin_call(fallback_value={})
  def process(self, data: bytes) -> dict[str, Any]:
      return self._plugin.process(data)
Тестирование плагинов упрощается благодаря протоколам. Создаёшь мок-менеджер, проверяешь изолированно:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import pytest
 
class MockPluginManager:
  """Упрощённый менеджер для тестов"""
  
  def __init__(self):
      self.plugins: dict[str, DataPlugin] = {}
  
  def register(self, plugin: DataPlugin) -> None:
      self.plugins[plugin.name] = plugin
 
def test_json_plugin():
  manager = MockPluginManager()
  plugin = JSONPlugin()
  manager.register(plugin)
  
  # Проверяем обработку
  data = b'{"test": "value"}'
  result = plugin.process(data)
  
  assert "data" in result
  assert result["data"]["test"] == "value"
  assert result["plugin"] == "json_processor"
 
def test_plugin_protocol_compliance():
  """Проверка соответствия протоколу"""
  plugin = JSONPlugin()
  
  # Статический анализатор проверит эти присваивания
  p: DataPlugin = plugin
  
  assert callable(p.process)
  assert callable(p.can_handle)
  assert isinstance(p.name, str)
Архитектура фреймворка позволяет менять реализацию менеджера без правки плагинов. Три недели назад переписал загрузку с динамического импорта на точки входа setuptools. Ни один плагин не потребовал изменений - контракт остался прежним, изменилась только механика обнаружения. Вот что даёт протокольный подход к проектированию.

Python 35 Выполнить файл из python shell
Есть файл do.py : print('start') import os import sys import re import inspect def...

Python - момент истины. Python - как оружие возмездие против системы
Какие модули в python мне нужны для взлома баз данных? Перехвата информации? Внедрения в систему? ...

Сложности с переходом с python 2.x на python 3.x
def _load_config(self): for fn in CONFIG_FILES: fn = os.path.expanduser(fn) ...

Изменение кода запроса с Python 2 на Python 3
Доброго времени суток. Я пишу программу и для её реализации мне необходимо, чтобы она делала...

Порт pyqt5 (python 3.5) программы на android - Python
Подскажите пожалуйста возможно ли программу написанную на python методами pyqt5 переделать под...

Перевод кода из Pascal в Python - Python
Имеется код программы на языке Pascal, требуется перевести его в Python. Я не могу перевести его в...

Не могу получить ответ от python скрипта и на его основе создать список (зависимые списки js ajax python)
Привет! Есть необходимость сделать динамические списки при помощи js, ajax jQuery, Python. Данные...

Cx_freeze python error in main script как исправить- Python
Пытался создать из .py .exe , но при запуске .exe получаю ошибку вот код setup.py from cx_Freeze...

Перевод из Python в C/C++ - Python
перевести все программы с файла в C\C++

Перевод кода с C++ на Python - Python
#include &lt;iostream&gt; #include &lt;fstream&gt; #include &lt;string&gt; #include &lt;cstdlib&gt; using namespace...

Maching pursuit. Согласованный поиск на Python. Требуется сам алгоритм, написанный на Python
Требуется сам алгоритм, написанный на Python. Заранее спасибо.

Fatal error in launcher: Unable to create process using '“d:\autocompiler\temp\portable python-3.7.3 x64\app\python\pyth
Я использую портативный Python3.7.3. в моем USB-диске. Когда я пытаюсь установить pandos, я получаю...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
И решил я переделать этот ноут в машину для распределенных вычислений
Programma_Boinc 09.11.2025
И решил я переделать этот ноут в машину для распределенных вычислений Всем привет. А вот мой компьютер, переделанный из ноутбука. Был у меня ноут асус 2011 года. Со временем корпус превратился. . .
Мысли в слух
kumehtar 07.11.2025
Заметил среди людей, что по-настоящему верная дружба бывает между теми, с кем нечего делить.
Новая зверюга
volvo 07.11.2025
Подарок на Хеллоуин, и теперь у нас кроме Tuxedo Cat есть еще и щенок далматинца: Хочу еще Симбу взять, очень нравится. . .
Инференс ML моделей в Java: TensorFlow, DL4J и DJL
Javaican 05.11.2025
Python захватил мир машинного обучения - это факт. Но когда дело доходит до продакшена, ситуация не так однозначна. Помню проект в крупном банке три года назад: команда data science натренировала. . .
Mapped types (отображённые типы) в TypeScript
Reangularity 03.11.2025
Mapped types работают как конвейер - берут существующую структуру и производят новую по заданным правилам. Меняют модификаторы свойств, трансформируют значения, фильтруют ключи. Один раз описал. . .
Адаптивная случайность в Unity: динамические вероятности для улучшения игрового дизайна
GameUnited 02.11.2025
Мой знакомый геймдизайнер потерял двадцать процентов активной аудитории за неделю. А виновником оказался обычный генератор псевдослучайных чисел. Казалось бы - добавил в карточную игру случайное. . .
Протоколы в Python
py-thonny 31.10.2025
Традиционная утиная типизация работает просто: попробовал вызвать метод, получилось - отлично, не получилось - упал с ошибкой в рантайме. Протоколы добавляют сюда проверку на этапе статического. . .
C++26: Read-copy-update (RCU)
bytestream 30.10.2025
Прошло почти двадцать лет с тех пор, как производители процессоров отказались от гонки мегагерц и перешли на многоядерность. И знаете что? Мы до сих пор спотыкаемся о те же грабли. Каждый раз, когда. . .
Изображения webp на старых x32 ОС Windows XP и Windows 7
Argus19 30.10.2025
Изображения webp на старых x32 ОС Windows XP и Windows 7 Чтобы решить задачу, использовал интернет: поисковики Google и Yandex, а также подсказки Deep Seek. Как оказалось, чтобы создать. . .
Passkey в ASP.NET Core identity
stackOverflow 29.10.2025
Пароли мертвы. Нет, серьезно - я повторяю это уже лет пять, но теперь впервые за это время чувствую, что это не просто красивые слова. В . NET 10 команда Microsoft внедрила поддержку Passkey прямо в. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru