← Назад к вопросам
Когда лучше создавать интерфейс на основе Protocol в Python?
2.7 Senior🔥 111 комментариев
#Python Core#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Когда лучше создавать интерфейс на основе Protocol в Python?
Python Protocol (из typing) — это инструмент для структурной типизации (structural typing). Разберу, когда его использовать и почему он лучше традиционного наследования.
Что такое Protocol?
Protocol определяет контракт методов, но БЕЗ наследования.
from typing import Protocol
# Традиционный подход — наследование (nominal typing)
class AnimalBase(ABC):
@abstractmethod
def make_sound(self) -> str:
pass
class Dog(AnimalBase): # ДОЛЖЕН наследоваться
def make_sound(self) -> str:
return "Woof!"
# Protocol подход — структурная типизация (structural typing)
class Animal(Protocol):
def make_sound(self) -> str: ...
class Cat: # НЕ наследуется от Animal!
def make_sound(self) -> str:
return "Meow!"
# Но type checker считает Cat совместимым с Animal
def make_animal_sound(animal: Animal) -> str:
return animal.make_sound()
dog = Dog()
cat = Cat()
make_animal_sound(dog) # OK
make_animal_sound(cat) # OK (не наследуется, но структурно совместимо!)
Когда использовать Protocol?
1. Когда у тебя есть интерфейсы из внешних библиотек
# Проблема: хочешь работать с разными логгерами
import logging
from logging import Logger
class CustomLogger:
def info(self, msg: str): ...
def error(self, msg: str): ...
# Обе библиотеки имеют разные базовые классы
# Но структурно одинаковы!
# Решение — Protocol
from typing import Protocol
class Logger(Protocol):
def info(self, msg: str) -> None: ...
def error(self, msg: str) -> None: ...
def warning(self, msg: str) -> None: ...
def process_data(logger: Logger):
logger.info("Processing...")
# Работает с ЛЮБЫМ объектом, имеющим эти методы
# logging.Logger? OK
# CustomLogger? OK
# SomeThirdPartyLogger? OK
2. Утиная типизация с проверкой типов
# Python философия: "утиная типизация"
# "Если ходит как утка и крякает как утка — это утка"
# БЕЗ Protocol — type checker не поймёт
def feed_animal(animal): # Что это может быть?
animal.eat()
animal.sleep()
# С Protocol — явно и проверяемо
from typing import Protocol
class Eatable(Protocol):
def eat(self) -> None: ...
def sleep(self) -> None: ...
def feed_animal(animal: Eatable) -> None:
animal.eat()
animal.sleep()
class Dog:
def eat(self): print("Eating")
def sleep(self): print("Sleeping")
class Robot:
def eat(self): print("Charging")
def sleep(self): print("Standby")
feed_animal(Dog()) # OK
feed_animal(Robot()) # OK — структурно совместимо!
3. Когда наследование создаёт ненужные зависимости
# ПЛОХО — сильная связанность
class BaseDataProcessor(ABC):
@abstractmethod
def process(self, data): pass
class MyProcessor(BaseDataProcessor): # Зависит от BaseDataProcessor
def process(self, data):
return data.upper()
# ХОРОШО — слабая связанность
from typing import Protocol
class DataProcessor(Protocol):
def process(self, data: str) -> str: ...
class MyProcessor: # НЕ зависит ни от чего!
def process(self, data: str) -> str:
return data.upper()
# Фидоров из другой кодовой базы
class ThirdPartyProcessor:
def process(self, data: str) -> str:
return data.lower()
def handle_data(processor: DataProcessor, text: str) -> str:
return processor.process(text)
handle_data(MyProcessor(), "Hello") # OK
handle_data(ThirdPartyProcessor(), "Hello") # OK
4. Когда нужна гибкость в тестировании
from typing import Protocol
from unittest.mock import MagicMock
class DatabaseConnection(Protocol):
def query(self, sql: str) -> list:
...
def execute(self, sql: str) -> None:
...
class UserRepository:
def __init__(self, db: DatabaseConnection):
self.db = db
def get_user(self, user_id: int) -> dict:
return self.db.query(f"SELECT * FROM users WHERE id={user_id}")[0]
# В тестах используем mock
def test_get_user():
mock_db = MagicMock(spec=DatabaseConnection) # Соответствует Protocol
mock_db.query.return_value = [{"id": 1, "name": "John"}]
repo = UserRepository(mock_db)
user = repo.get_user(1)
assert user["name"] == "John"
5. Когда работаешь с колбэками и функциональным стилем
from typing import Protocol, Callable
class Validator(Protocol):
def validate(self, value: str) -> bool: ...
class EmailValidator:
def validate(self, value: str) -> bool:
return "@" in value
class LengthValidator:
def validate(self, value: str) -> bool:
return len(value) > 5
class FormField:
def __init__(self, validator: Validator):
self.validator = validator
def is_valid(self, value: str) -> bool:
return self.validator.validate(value)
# Работает с ЛЮБЫМ объектом с методом validate
field1 = FormField(EmailValidator())
field2 = FormField(LengthValidator())
Protocol vs ABC (Abstract Base Class)
ABC — номинальная типизация (nominal typing)
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def make_sound(self) -> str:
pass
class Dog(Animal): # ДОЛЖЕН явно наследоваться
def make_sound(self) -> str:
return "Woof!"
class Cat: # Забыл наследоваться
def make_sound(self) -> str:
return "Meow!"
dog: Animal = Dog() # OK
cat: Animal = Cat() # Ошибка типа!
Protocol — структурная типизация (structural typing)
from typing import Protocol
class Animal(Protocol):
def make_sound(self) -> str: ...
class Dog: # НЕ наследуется
def make_sound(self) -> str:
return "Woof!"
class Cat: # НЕ наследуется
def make_sound(self) -> str:
return "Meow!"
dog: Animal = Dog() # OK — структурно совместимо
cat: Animal = Cat() # OK — структурно совместимо
Сравнительная таблица
| Аспект | ABC | Protocol |
|---|---|---|
| Наследование | Требуется явное | Не требуется |
| Связанность | Высокая | Низкая |
| Совместимость с внешним кодом | Сложная | Легкая |
| Runtime проверка | isinstance() | Только type checking |
| Идеально для | Иерархии классов | Интерфейсы поведения |
Практический пример: реальная архитектура
from typing import Protocol
from abc import ABC, abstractmethod
# Protocol для внешних зависимостей (не контролируем)
class MessageBroker(Protocol):
def send(self, topic: str, message: str) -> None: ...
def subscribe(self, topic: str, callback) -> None: ...
# ABC для своего кода (иерархия)
class NotificationService(ABC):
@abstractmethod
def send_notification(self, user_id: int, message: str) -> None:
pass
class EmailNotificationService(NotificationService):
def __init__(self, broker: MessageBroker):
self.broker = broker
def send_notification(self, user_id: int, message: str) -> None:
self.broker.send(f"email.{user_id}", message)
class SMSNotificationService(NotificationService):
def __init__(self, broker: MessageBroker):
self.broker = broker
def send_notification(self, user_id: int, message: str) -> None:
self.broker.send(f"sms.{user_id}", message)
# Используем RabbitMQ (не наследуется от MessageBroker)
import pika
class RabbitMQBroker: # Protocol не требует наследования!
def send(self, topic: str, message: str) -> None:
# Реализация
pass
def subscribe(self, topic: str, callback) -> None:
# Реализация
pass
broker = RabbitMQBroker() # Совместимо с Protocol
service = EmailNotificationService(broker) # OK
Рекомендации по выбору
Используй Protocol когда:
- Работаешь с внешними библиотеками
- Хочешь максимальную гибкость
- Нет иерархии классов
- Важна слабая связанность
- Пишешь инструменты/библиотеки
Используй ABC когда:
- Есть иерархия в твоём коде
- Контролируешь все реализации
- Нужны runtime проверки (isinstance)
- Иерархия логична и стабильна
Итог
Protocol — это способ сказать type checker'у: "Мне нужен объект с этими методами, не важно его тип и иерархия." Это философия Python в чистом виде: утиная типизация с полной поддержкой type checking.