← Назад к вопросам

Когда лучше создавать интерфейс на основе 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 — структурно совместимо

Сравнительная таблица

АспектABCProtocol
НаследованиеТребуется явноеНе требуется
СвязанностьВысокаяНизкая
Совместимость с внешним кодомСложнаяЛегкая
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.