Полиморфизм — это способность разных объектов отвечать на одни и те же вызовы, реализуя их по-своему. Слово происходит от греческого «много форм» — и это точное описание: один и тот же интерфейс, множество реализаций.
Самый наглядный способ понять, зачем нужен полиморфизм — посмотреть на код без него. Предположим, у нас есть несколько типов уведомлений: email, SMS и push-уведомление. Без полиморфизма код рассылки выглядел бы так:
def send_notification(notification, notification_type):
if notification_type == "email":
# логика отправки email
print(f"Email: {notification['body']} → {notification['recipient']}")
elif notification_type == "sms":
# логика отправки SMS
print(f"SMS: {notification['body']} → {notification['phone']}")
elif notification_type == "push":
# логика отправки push
print(f"Push: {notification['body']} → {notification['device_token']}")
else:
raise ValueError(f"Неизвестный тип: {notification_type}")Каждый раз, когда нужно добавить новый тип уведомления, эту функцию нужно изменить. Она знает о всех типах, она содержит логику всех типов, она нарушает принцип открытости/закрытости: открыт для расширения — закрыт для изменения.
С полиморфизмом:
def send_notification(notification):
notification.send() # каждый объект знает, как отправлять себяФункция больше не знает о типах. Она просто вызывает send() — а каждый объект реализует этот метод по-своему. Добавление нового типа уведомления не требует изменения этой функции.
В Python существуют две модели полиморфизма. Первая — через наследование: все объекты унаследованы от общего базового класса. Вторая — через duck typing: объекты вообще не связаны иерархией, но реализуют одинаковый интерфейс. Python поддерживает обе модели, и понимание обеих — ключ к написанию гибкого кода.
Классический подход к полиморфизму: базовый класс определяет интерфейс, дочерние классы предоставляют конкретные реализации:
class Notification:
def __init__(self, recipient, subject, body):
self.recipient = recipient
self.subject = subject
self.body = body
def send(self):
raise NotImplementedError(
f"{self.__class__.__name__} должен реализовать метод send()"
)
def __repr__(self):
return f"{self.__class__.__name__}(recipient={self.recipient!r})"
class EmailNotification(Notification):
def send(self):
print(f"[Email] → {self.recipient}: {self.subject}")
return {"type": "email", "status": "sent"}
class SMSNotification(Notification):
def __init__(self, phone, subject, body):
super().__init__(phone, subject, body)
def send(self):
print(f"[SMS] → {self.recipient}: {self.body[:160]}")
return {"type": "sms", "status": "sent"}
class PushNotification(Notification):
def __init__(self, device_token, subject, body):
super().__init__(device_token, subject, body)
def send(self):
print(f"[Push] → {self.recipient}: {self.subject}")
return {"type": "push", "status": "sent"}
def notify_all(notifications):
"""
Эта функция не знает и не хочет знать о конкретных типах уведомлений.
Она просто вызывает send() — полиморфизм делает остальное.
"""
results = []
for notification in notifications:
result = notification.send()
results.append(result)
return results
# Все объекты разных типов, но один код их обрабатывает
notifications = [
EmailNotification("alice@example.com", "Добро пожаловать", "Привет!"),
SMSNotification("+79001234567", "Код", "Ваш код: 1234"),
PushNotification("device_abc", "Новость", "Вышло обновление"),
]
results = notify_all(notifications)
# [Email] → alice@example.com: Добро пожаловать
# [SMS] → +79001234567: Ваш код: 1234
# [Push] → device_abc: НовостьКлючевой момент: функция notify_all написана один раз и никогда не потребует изменений при добавлении нового типа уведомления. Добавьте SlackNotification(Notification) с методом send() — и функция автоматически заработает с ним.
В Python объект не обязан наследоваться от определённого класса, чтобы быть использованным в определённом контексте. Достаточно, чтобы он реализовал нужные методы. Это называется duck typing — термин восходит к поговорке: «Если что-то ходит как утка и крякает как утка, то это, вероятно, утка».
Классический пример в Python: функция, работающая с «файлоподобными» объектами. Стандартная библиотека Python не требует, чтобы вы передавали именно файл — она требует объект с методом write():
import json
# Стандартный файл
with open("data.json", "w") as f:
json.dump({"key": "value"}, f) # f — файл
# io.StringIO — не файл, но работает так же
import io
buffer = io.StringIO()
json.dump({"key": "value"}, buffer) # buffer — не файл, но имеет write()
print(buffer.getvalue()) # '{"key": "value"}'
# Наш собственный класс — тоже работает, если реализует write()
class LogWriter:
def __init__(self):
self.log = []
def write(self, text):
self.log.append(text)
return len(text)
log = LogWriter()
json.dump({"key": "value"}, log) # работает! json.dump просто вызывает write()
print(log.log) # ['{"key": "value"}']json.dump не проверяет isinstance(f, io.IOBase). Она просто вызывает f.write(). Любой объект с методом write() автоматически является «файлоподобным» с точки зрения json.dump.
Теперь применим этот принцип к системе уведомлений. Сделаем её независимой от базового класса:
class SlackNotification:
"""
Этот класс НЕ наследует от Notification.
Но он реализует метод send() — этого достаточно для duck typing.
"""
def __init__(self, channel, message):
self.channel = channel
self.message = message
def send(self):
print(f"[Slack] #{self.channel}: {self.message}")
return {"type": "slack", "status": "sent"}
# Функция notify_all работает с любым объектом, имеющим метод send()
slack = SlackNotification("engineering", "Сервер перезапущен")
email = EmailNotification("alice@example.com", "Alert", "Сервер перезапущен")
# Оба объекта работают в одной функции — хотя SlackNotification не является Notification
notify_all([email, slack])Это принципиально важный момент: duck typing позволяет использовать объекты вместе, даже если они не связаны общим предком. Ограничение — только наличие нужных методов.
В Python существуют неформальные «протоколы» — наборы методов, которые объект должен реализовать, чтобы работать в определённом контексте. Вы уже знаете большинство из них по модулю о магических методах:
# Протокол итерации: __iter__ и __next__
# Любой объект с этими методами работает в for-цикле
for item in my_object: # Python вызывает __iter__, затем __next__
pass
# Протокол контекстного менеджера: __enter__ и __exit__
# Любой объект с этими методами работает в with-блоке
with my_object as resource:
pass
# Протокол последовательности: __len__ и __getitem__
# Любой объект с этими методами работает в функциях типа len(), []
print(len(my_object))
print(my_object[0])
# Протокол сравнения: __eq__, __lt__ и т.д.
# Любой объект с этими методами работает в sorted(), min(), max()
sorted_list = sorted([my_obj1, my_obj2, my_obj3])Рассмотрим конкретный пример: создадим класс, который ведёт себя как список, не наследуясь от него:
class ResponseList:
"""
Коллекция HTTP-ответов. Реализует протокол последовательности
без наследования от list.
"""
def __init__(self, responses=None):
self._responses = list(responses or [])
def __len__(self):
return len(self._responses)
def __getitem__(self, index):
return self._responses[index]
def __iter__(self):
return iter(self._responses)
def append(self, response):
self._responses.append(response)
responses = ResponseList([
{"status": 200, "body": "OK"},
{"status": 404, "body": "Not Found"},
{"status": 500, "body": "Error"},
])
# Работает с len() — благодаря __len__
print(len(responses)) # 3
# Работает в for-цикле — благодаря __iter__
for r in responses:
print(r["status"])
# Работает с индексом — благодаря __getitem__
print(responses[0]) # {'status': 200, 'body': 'OK'}
# Работает в list comprehension — благодаря __iter__
errors = [r for r in responses if r["status"] >= 400]
print(errors) # [{'status': 404, ...}, {'status': 500, ...}]
# НЕ является экземпляром list
print(isinstance(responses, list)) # False
# Но ведёт себя как список там, где ожидается последовательностьПри работе с duck typing возникает вопрос: как обработать ситуацию, когда объект не реализует нужный метод? Python-сообщество выработало два подхода.
LBYL (Look Before You Leap) — «смотри перед прыжком»: проверяем наличие атрибута/метода перед его использованием:
def send_notification(notification):
if hasattr(notification, 'send') and callable(notification.send):
return notification.send()
else:
raise TypeError(f"Объект {notification!r} не поддерживает отправку")EAFP (Easier to Ask Forgiveness than Permission) — «проще попросить прощения, чем разрешения»: просто вызываем метод и обрабатываем исключение:
def send_notification(notification):
try:
return notification.send()
except AttributeError:
raise TypeError(f"Объект {notification!r} не имеет метода send()")Python-сообщество предпочитает EAFP — это более идиоматично. EAFP лаконичнее, не страдает от race conditions (объект мог потерять атрибут между проверкой и использованием), и явно описывает, какое исключение мы ожидаем.
Однако в контексте обработки разных типов объектов часто используется гибридный подход:
def process_data(data):
"""
Обрабатывает данные разных форматов.
Предпочитает duck typing, но проверяет тип для специальных случаев.
"""
# Специальная обработка для строк — они итерируемы, но мы хотим другое поведение
if isinstance(data, str):
return data.strip()
# Для всего остального — пробуем работать как с итерируемым
try:
return [process_data(item) for item in data]
except TypeError:
return dataВ уроке 19 мы познакомились с isinstance() как инструментом проверки принадлежности к иерархии. Теперь рассмотрим более тонкий вопрос: когда использование isinstance() в полиморфном коде является антипаттерном, а когда — легитимным инструментом.
Антипаттерн: замена полиморфизма проверками типа.
# ПЛОХО: мы сами занимаемся диспетчеризацией — это задача полиморфизма
def serialize(obj):
if isinstance(obj, EmailNotification):
return {"type": "email", "to": obj.recipient}
elif isinstance(obj, SMSNotification):
return {"type": "sms", "phone": obj.recipient}
elif isinstance(obj, PushNotification):
return {"type": "push", "token": obj.recipient}
# При добавлении нового типа нужно менять эту функцию!Это именно та цепочка if/elif, от которой полиморфизм должен избавить.
Легитимный случай 1: диспетчеризация по типу для встроенных типов.
Когда у вас нет контроля над объектами (например, это встроенные типы Python), добавить к ним метод нельзя. Здесь isinstance() — единственный выход:
import json
import datetime
from decimal import Decimal
class CustomJSONEncoder(json.JSONEncoder):
"""
Расширенный JSON-энкодер, поддерживающий дополнительные типы.
isinstance() здесь легитимен: мы не можем добавить метод к datetime или Decimal.
"""
def default(self, obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
if isinstance(obj, datetime.date):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
if isinstance(obj, set):
return sorted(list(obj))
# Для пользовательских объектов — duck typing
if hasattr(obj, 'to_dict'):
return obj.to_dict()
return super().default(obj)
data = {
"created_at": datetime.datetime.now(),
"price": Decimal("19.99"),
"tags": {"python", "web", "api"},
}
print(json.dumps(data, cls=CustomJSONEncoder, indent=2))Легитимный случай 2: валидация входных данных.
def process_payment(amount, currency):
if not isinstance(amount, (int, float, Decimal)):
raise TypeError(
f"amount должен быть числом, получен {type(amount).__name__}"
)
if not isinstance(currency, str):
raise TypeError(
f"currency должна быть строкой, получен {type(currency).__name__}"
)
# Основная логика...Легитимный случай 3: специальная обработка конкретного подтипа.
def log_response(response):
"""Логирование ответа с дополнительной информацией для ошибок."""
print(f"Status: {response.status_code}")
# Для ошибочных ответов — дополнительное логирование
if isinstance(response, ErrorResponse):
print(f"Error code: {response.error_code}")
print(f"Stack trace: {response.traceback}")
# Для остальных — только базовая информацияisinstance() может принимать не только один тип, но и кортеж типов. Это позволяет проверить принадлежность к любому из нескольких типов за один вызов:
def format_value(value):
"""Форматирует значение для отображения в API-ответе."""
if isinstance(value, bool):
# bool ОБЯЗАТЕЛЬНО проверяем перед int — bool является подклассом int!
return str(value).lower() # True → "true", False → "false"
if isinstance(value, (int, float, Decimal)):
return str(value)
if isinstance(value, (list, tuple)):
return [format_value(item) for item in value]
if isinstance(value, dict):
return {k: format_value(v) for k, v in value.items()}
if isinstance(value, str):
return value
if isinstance(value, datetime.datetime):
return value.isoformat()
# Для всего остального — duck typing
if hasattr(value, '__str__'):
return str(value)
raise TypeError(f"Не удаётся сериализовать объект типа {type(value).__name__}")
print(format_value(True)) # "true"
print(format_value(42)) # "42"
print(format_value([1, True, "a"])) # ["1", "true", "a"]
print(format_value({"x": 3.14})) # {"x": "3.14"}Обратите внимание на критически важную деталь: проверка isinstance(value, bool) идёт раньше isinstance(value, (int, float)). Это необходимо, потому что bool является подклассом int в Python:
print(isinstance(True, int)) # True — bool является подклассом int!
print(isinstance(True, bool)) # True
print(isinstance(1, bool)) # False — int НЕ является bool
# Неправильный порядок приведёт к ошибке:
value = True
if isinstance(value, int): # True пройдёт эту проверку!
return str(value) # вернёт "1", а не "true"Кратко: ABC — это способ задать интерфейс (контракт) и контролировать, кто ему соответствует.
Это один из самых неочевидных механизмов Python. Абстрактные базовые классы (ABC) позволяют зарегистрировать класс как «виртуальный подкласс» другого класса — без реального наследования. После регистрации isinstance() будет возвращать True для объектов этого класса.
Предварительное знакомство — подробно ABC разберём в следующем уроке:
from abc import ABC, abstractmethod
class Sendable(ABC):
"""
Абстрактный базовый класс для отправляемых объектов.
Определяет протокол: объект должен иметь метод send().
"""
@abstractmethod
def send(self):
pass
@classmethod
def __subclasshook__(cls, subclass):
"""
Хук для проверки: является ли subclass виртуальным подклассом Sendable?
Возвращает True, если у класса есть метод send.
Это позволяет isinstance() работать с любым классом, имеющим send().
"""
if cls is Sendable:
return hasattr(subclass, 'send') and callable(getattr(subclass, 'send'))
return NotImplemented
class TelegramNotification:
"""
Этот класс НЕ наследует ни от Notification, ни от Sendable.
Но у него есть метод send() — благодаря __subclasshook__
он пройдёт проверку isinstance(..., Sendable).
"""
def __init__(self, chat_id, text):
self.chat_id = chat_id
self.text = text
def send(self):
print(f"[Telegram] chat_id={self.chat_id}: {self.text}")
return {"type": "telegram", "status": "sent"}
telegram = TelegramNotification(123456, "Привет!")
# Без реального наследования — isinstance возвращает True благодаря __subclasshook__
print(isinstance(telegram, Sendable)) # True
print(issubclass(TelegramNotification, Sendable)) # True
# Работает в полиморфном коде
def send_if_sendable(obj):
if isinstance(obj, Sendable):
return obj.send()
raise TypeError(f"Объект {obj!r} не является отправляемым")
send_if_sendable(telegram) # [Telegram] chat_id=123456: Привет!Это мощный механизм: вы можете определить протокол (набор методов) через ABC с __subclasshook__, и тогда isinstance() автоматически будет проверять, реализует ли объект этот протокол — без необходимости наследования. Это сочетание преимуществ duck typing (не нужно наследоваться) и явной типизации (можно использовать isinstance()).
Метапрограммирование — это работа с классами и типами как с данными, позволяющая динамически управлять логикой программы. Код принимает решения, основываясь на типах (классах), а не только на данных.
issubclass() проверяет отношение между классами. Кроме базового использования из урока 19, у неё есть важные применения в паттернах, близких к метапрограммированию.
Паттерн «реестр обработчиков»: система регистрирует обработчики для разных базовых классов и использует issubclass() для диспетчеризации:
class EventRegistry:
"""
Реестр обработчиков событий.
Диспетчеризация основана на иерархии типов событий.
"""
def __init__(self):
self._handlers = {} # {EventClass: [handler_functions]}
def register(self, event_class, handler):
"""Регистрирует обработчик для класса события."""
if event_class not in self._handlers:
self._handlers[event_class] = []
self._handlers[event_class].append(handler)
def dispatch(self, event):
"""
Находит все обработчики для данного события.
Учитывает иерархию: обработчик для базового класса
вызывается для всех дочерних.
"""
for event_class, handlers in self._handlers.items():
if isinstance(event, event_class): # isinstance учитывает наследование
for handler in handlers:
handler(event)
# Определяем иерархию событий
class BaseEvent:
def __init__(self, source):
self.source = source
class UserEvent(BaseEvent):
def __init__(self, source, user_id):
super().__init__(source)
self.user_id = user_id
class UserRegisteredEvent(UserEvent):
def __init__(self, source, user_id, email):
super().__init__(source, user_id)
self.email = email
class UserDeletedEvent(UserEvent):
def __init__(self, source, user_id):
super().__init__(source, user_id)
# Создаём реестр и регистрируем обработчики
registry = EventRegistry()
# Обработчик для ВСЕХ событий
registry.register(BaseEvent, lambda e: print(f"[LOG] Событие от {e.source}"))
# Обработчик для ВСЕХ пользовательских событий
registry.register(UserEvent, lambda e: print(f"[AUDIT] Изменение пользователя {e.user_id}"))
# Обработчик только для регистрации
registry.register(
UserRegisteredEvent,
lambda e: print(f"[EMAIL] Отправляем приветствие на {e.email}")
)
# Диспетчеризация
event = UserRegisteredEvent("api", user_id=42, email="alice@example.com")
registry.dispatch(event)
# [LOG] Событие от api ← BaseEvent handler
# [AUDIT] Изменение пользователя 42 ← UserEvent handler
# [EMAIL] Отправляем приветствие на alice@example.com ← UserRegisteredEvent handlerissubclass() для проверки в декораторах:
def requires_base_model(cls):
"""
Декоратор, проверяющий что класс наследует от BaseModel.
Использует issubclass() для проверки иерархии.
"""
if not issubclass(cls, BaseModel):
raise TypeError(
f"Класс {cls.__name__} должен наследовать от BaseModel"
)
return cls
class BaseModel:
pass
@requires_base_model
class UserModel(BaseModel): # OK — является подклассом BaseModel
pass
try:
@requires_base_model
class WrongModel: # TypeError — не наследует от BaseModel
pass
except TypeError as e:
print(e) # Класс WrongModel должен наследовать от BaseModelТри сценария, демонстрирующих разницу:
class Animal:
pass
class Dog(Animal):
pass
dog = Dog()
# Сценарий 1: isinstance() учитывает иерархию
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True — Dog является Animal
# type() не учитывает иерархию
print(type(dog) == Dog) # True
print(type(dog) == Animal) # False — type() возвращает точный тип
# Сценарий 2: type() для точной проверки (редкий легитимный случай)
def process_number(n):
if type(n) is int: # именно int, не bool, не подкласс
return n * 2
raise TypeError("Нужен именно int, не подкласс")
print(process_number(5)) # 10
try:
process_number(True) # True — это bool, подкласс int
except TypeError as e:
print(e) # Нужен именно int, не подкласс
# Сценарий 3: isinstance() с кортежем — type() так не умеет
value = 42
print(isinstance(value, (int, float, str))) # True
# type() потребует несколько проверок:
print(type(value) in (int, float, str)) # True — но менее читаемоПравило: используйте isinstance() по умолчанию — он корректно работает с наследованием и виртуальными подклассами. Используйте type() is только в специфических случаях, когда вам нужна точная проверка типа без учёта наследования.
Реализуем гибкую систему сериализации, сочетающую duck typing, isinstance() для встроенных типов и ABC для проверки протокола:
import json
import datetime
from decimal import Decimal
from abc import ABC, abstractmethod
class Serializable(ABC):
"""Протокол для объектов, умеющих сериализовать себя."""
@abstractmethod
def to_dict(self) -> dict:
pass
@classmethod
def __subclasshook__(cls, subclass):
if cls is Serializable:
return hasattr(subclass, 'to_dict') and callable(getattr(subclass, 'to_dict'))
return NotImplemented
class WebAPISerializer:
"""
Сериализатор для Web API. Превращает любые объекты в JSON-совместимый формат.
Стратегия:
1. Duck typing: если есть to_dict() — используем его
2. isinstance() для встроенных типов, которые нельзя расширить
3. EAFP: при ошибке — понятное сообщение
"""
def serialize(self, obj):
"""Основной метод сериализации."""
return self._convert(obj)
def _convert(self, obj):
# Duck typing первым: если объект умеет сериализовать себя — доверяем ему
if isinstance(obj, Serializable):
return self._convert(obj.to_dict())
# Встроенные примитивы — уже JSON-совместимы
if isinstance(obj, (int, float, str, bool)) or obj is None:
return obj
# Дата/время — особый формат
if isinstance(obj, datetime.datetime):
return {"__type": "datetime", "value": obj.isoformat()}
if isinstance(obj, datetime.date):
return {"__type": "date", "value": obj.isoformat()}
# Decimal — конвертируем в строку для точности
if isinstance(obj, Decimal):
return {"__type": "decimal", "value": str(obj)}
# Коллекции — рекурсивно
if isinstance(obj, dict):
return {str(k): self._convert(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [self._convert(item) for item in obj]
if isinstance(obj, set):
return sorted([self._convert(item) for item in obj])
# Последний шанс: объект с __dict__ (любой пользовательский класс)
if hasattr(obj, '__dict__'):
return self._convert(vars(obj))
raise TypeError(
f"Невозможно сериализовать объект типа {type(obj).__name__}"
)
def to_json(self, obj, **kwargs):
"""Конвертирует в строку JSON."""
converted = self.serialize(obj)
return json.dumps(converted, **kwargs)
# Объекты с to_dict() — работают через duck typing
class UserProfile:
def __init__(self, user_id, username, email):
self.user_id = user_id
self.username = username
self.email = email
self.registered_at = datetime.date.today()
def to_dict(self):
return {
"id": self.user_id,
"username": self.username,
"email": self.email,
"registered": self.registered_at,
}
class OrderItem:
def __init__(self, product_name, quantity, price):
self.product_name = product_name
self.quantity = quantity
self.price = Decimal(str(price))
def to_dict(self):
return {
"product": self.product_name,
"qty": self.quantity,
"price": self.price,
"total": self.price * self.quantity,
}
serializer = WebAPISerializer()
# Проверяем isinstance с Serializable через __subclasshook__
print(isinstance(UserProfile(1, "a", "b"), Serializable)) # True
# Сериализуем сложную структуру
response_data = {
"user": UserProfile(42, "alice", "alice@example.com"),
"order": {
"id": 1001,
"created_at": datetime.datetime.now(),
"items": [
OrderItem("Ноутбук", 1, "89990.00"),
OrderItem("Мышь", 2, "1500.00"),
],
"discount": Decimal("0.10"),
},
"tags": {"premium", "new_user"},
}
result = serializer.to_json(response_data, indent=2, ensure_ascii=False)
print(result)Полный пример системы событий, сочетающей полиморфизм через наследование, duck typing и isinstance() для диспетчеризации:
from typing import Callable, List
class Event:
"""Базовое событие системы."""
def __init__(self, source: str):
self.source = source
self.timestamp = datetime.datetime.now()
def __repr__(self):
return f"{self.__class__.__name__}(source={self.source!r})"
class HTTPRequestEvent(Event):
def __init__(self, source, method, path, status_code):
super().__init__(source)
self.method = method
self.path = path
self.status_code = status_code
class UserActionEvent(Event):
def __init__(self, source, user_id, action):
super().__init__(source)
self.user_id = user_id
self.action = action
class ErrorEvent(Event):
def __init__(self, source, error_type, message, traceback=None):
super().__init__(source)
self.error_type = error_type
self.message = message
self.traceback = traceback
class EventBus:
"""
Шина событий. Поддерживает несколько паттернов подписки:
1. По конкретному типу события
2. По базовому типу (все дочерние тоже получают)
3. Duck typing: любой вызываемый объект является обработчиком
"""
def __init__(self):
self._subscribers: dict = {}
def subscribe(self, event_type, handler: Callable):
"""
Подписка на событие.
handler — любой вызываемый объект (функция, метод, класс с __call__).
Duck typing: не проверяем тип handler — просто вызываем его.
"""
if not callable(handler):
raise TypeError(f"Handler должен быть вызываемым, получен {type(handler)}")
if event_type not in self._subscribers:
self._subscribers[event_type] = []
self._subscribers[event_type].append(handler)
def publish(self, event: Event):
"""
Публикация события. Вызывает все подходящие обработчики.
isinstance() здесь легитимен: нам нужна диспетчеризация по типу.
"""
if not isinstance(event, Event):
raise TypeError(f"Ожидается Event, получен {type(event).__name__}")
called_handlers = set()
for event_type, handlers in self._subscribers.items():
if isinstance(event, event_type):
for handler in handlers:
handler_id = id(handler)
if handler_id not in called_handlers:
handler(event)
called_handlers.add(handler_id)
# Обработчики — любые вызываемые объекты (duck typing)
def log_all_events(event):
print(f"[LOG] {event.timestamp.strftime('%H:%M:%S')} — {event}")
def monitor_errors(event):
print(f"[ALERT] Ошибка: {event.error_type}: {event.message}")
class MetricsCollector:
"""Класс-обработчик — вызывается через __call__."""
def __init__(self):
self.request_count = 0
self.error_count = 0
def __call__(self, event):
if isinstance(event, HTTPRequestEvent):
self.request_count += 1
elif isinstance(event, ErrorEvent):
self.error_count += 1
print(f"[METRICS] Запросов: {self.request_count}, Ошибок: {self.error_count}")
bus = EventBus()
metrics = MetricsCollector()
bus.subscribe(Event, log_all_events) # все события
bus.subscribe(HTTPRequestEvent, metrics) # только HTTP-запросы
bus.subscribe(ErrorEvent, monitor_errors) # только ошибки
bus.subscribe(ErrorEvent, metrics) # ошибки тоже в метрики
# Публикуем события
bus.publish(HTTPRequestEvent("api", "GET", "/users", 200))
print()
bus.publish(ErrorEvent("db", "ConnectionError", "Соединение прервано"))isinstance(True, int) возвращает True. Это самая частая ловушка при работе с isinstance(). bool является подклассом int, поэтому True проходит проверку isinstance(obj, int). Всегда проверяйте bool перед int:
print(isinstance(True, int)) # True — неожиданно!
print(isinstance(True, bool)) # True
print(isinstance(1, bool)) # False — int не является bool
# Правильный порядок проверок:
def process(value):
if isinstance(value, bool): # СНАЧАЛА bool
return f"boolean: {value}"
if isinstance(value, int): # ПОТОМ int
return f"integer: {value}"issubclass() выбрасывает TypeError, если первый аргумент не класс. Это отличает её от isinstance(), которая принимает объекты:
obj = "hello"
isinstance(obj, str) # OK — obj это объект
try:
issubclass(obj, str) # TypeError — obj должен быть классом
except TypeError as e:
print(e) # issubclass() arg 1 must be a class
# Правильный способ проверить, является ли что-то классом:
print(isinstance(str, type)) # True — str это класс
print(isinstance(obj, type)) # False — "hello" это не классisinstance() с несуществующим типом — TypeError. Если второй аргумент не является классом или кортежем классов:
try:
isinstance(42, "int") # TypeError
except TypeError as e:
print(e) # isinstance() arg 2 must be a type, a tuple of types, or a unionПроизводительность isinstance(). В горячих циклах (обработка тысяч объектов в секунду) многократные проверки isinstance() могут стать узким местом. В таких случаях предпочтительнее полиморфизм через методы — он работает через таблицу виртуальных методов и значительно быстрее.
Полиморфизм — это способность разных объектов отвечать на одни и те же вызовы. В Python он реализуется двумя способами: через наследование (классический ООП) и через duck typing (объект не обязан быть подклассом — достаточно иметь нужные методы).
Duck typing — идиоматичный стиль Python. Функции пишутся для «протоколов» (наборов методов), а не для конкретных типов. Любой объект, реализующий нужные методы, автоматически работает в этом контексте.
isinstance() уместен для: диспетчеризации по типу встроенных объектов, валидации входных данных, специальной обработки конкретного подтипа. Антипаттерн: заменять полиморфизм длинными цепочками isinstance().
Абстрактные базовые классы с __subclasshook__ позволяют расширить поведение isinstance(): класс может пройти проверку без реального наследования, если реализует нужные методы. Это мост между duck typing и явной типизацией.
bool является подклассом int — всегда проверяйте bool перед int при использовании isinstance(). issubclass() принимает только классы как первый аргумент — для проверки, что объект является классом, используйте isinstance(obj, type).
В следующем уроке мы подробно рассмотрим абстрактные классы — модуль abc, декоратор @abstractmethod и то, как они формализуют протоколы, обеспечивая, что дочерние классы обязательно реализуют нужные методы.
- В чём принципиальное отличие полиморфизма через наследование и duck typing? Какой из них является более идиоматичным для Python?
- Что такое «протокол» в контексте duck typing? Приведите два примера протоколов из стандартной библиотеки Python.
- Когда использование
isinstance()в полиморфном коде является антипаттерном, а когда — легитимным инструментом? - Почему при проверке через
isinstance()нужно проверятьboolпередint? - Что такое EAFP и LBYL? Какой стиль предпочтителен в Python и почему?
- Что такое виртуальный подкласс и как его создать? Как это связывает duck typing и
isinstance()? - Чем
issubclass()отличается отisinstance()в плане допустимых аргументов? Как проверить, что объект является классом? - В примере с
EventBusиз лекции использованиеisinstance()в методеpublish()является легитимным, а не антипаттерном. Объясните почему.
Полиморфный рендерер
Создайте систему рендеринга HTML-компонентов. Базовый класс HTMLComponent с абстрактным методом (генерирует исключение NotImplementedError()) render() -> str. Дочерние классы:
Heading(HTMLComponent)— принимаетtextиlevel(1–6), рендерит<h1>text</h1>.Paragraph(HTMLComponent)— принимаетtext, рендерит<p>text</p>.Button(HTMLComponent)— принимаетtextиaction(строка-URL), рендерит<button onclick="...">text</button>.Link(HTMLComponent)— принимаетtextиhref, рендерит<a href="...">text</a>.
Напишите функцию render_page(components: list) -> str, которая рендерит список компонентов через полиморфизм (без проверки типов).
Добавьте класс RawHTML, который не наследует от HTMLComponent, но имеет метод render().
Убедитесь, что render_page() работает и с RawHTML через duck typing.
Пример использования:
components = [
Heading("Добро пожаловать", level=1),
Paragraph("Это наша платформа для разработчиков."),
Button("Начать", action="/register"),
Link("Документация", href="/docs"),
RawHTML("<hr/>"), # не является HTMLComponent, но работает
]
html = render_page(components)
print(html)
# <h1>Добро пожаловать</h1>
# <p>Это наша платформа для разработчиков.</p>
# <button onclick="/register">Начать</button>
# <a href="/docs">Документация</a>
# <hr/>Универсальный конвертер форматов
Напишите функцию convert_to_string(value) -> str, которая преобразует любое значение в строку для API-ответа.
Правила:
bool→"true"или"false"(доint!),intиfloat→ строка числа,None→"null",listиtuple→ рекурсивно конвертировать каждый элемент, объединить через", "и обернуть в"[...]",dict→ рекурсивно конвертировать ключи и значения, формат"{key: value, ...}",- объект с методом
__str__, не являющийся базовым типом → использоватьstr(obj).
Используйте правильный порядок isinstance() проверок.
Пример использования:
print(convert_to_string(True)) # "true"
print(convert_to_string(42)) # "42"
print(convert_to_string(None)) # "null"
print(convert_to_string([1, True, None, "hi"])) # "[1, true, null, hi]"
print(convert_to_string({"a": 1, "b": True})) # "{a: 1, b: true}"Система хранилищ с duck typing
Создайте три класса хранилищ данных, которые реализуют одинаковый интерфейс (duck typing — без общего базового класса):
InMemoryStorage— хранит данные в словаре_data.FileStorage— хранит данные в словаре_data. Имитирует хранение: приsave()выводит"[File] записано: key", приload()возвращает данные из внутреннего словаря.CacheStorage— хранит данные в словаре_data. Хранит временные метки в словаре_timestamps. Хранит данные с TTL (time-to-live). Приload(), если данные просрочены (TTL секунд прошло) — возвращаетNone. Время в секундах можно записать с помощьюtime.time().
Каждый класс должен иметь методы save(key, data), load(key), delete(key), exists(key), keys() - возвращает список ключей основного словаря класса.
Напишите функцию backup(source, destination), которая копирует все данные из одного хранилища в другое через их общий интерфейс (без isinstance()).
Продемонстрируйте, что все три хранилища взаимозаменяемы.
Пример использования:
mem = InMemoryStorage()
file_storage = FileStorage()
cache = CacheStorage(ttl=60)
mem.save("user:1", {"name": "Alice"})
mem.save("user:2", {"name": "Bob"})
backup(mem, file_storage) # Копируем из памяти в файл
backup(mem, cache) # Копируем из памяти в кеш
print(file_storage.load("user:1")) # {'name': 'Alice'}
print(cache.load("user:1")) # {'name': 'Alice'}
print(cache.exists("user:99")) # FalseПолиморфный pipeline обработки данных
Создайте систему pipeline для обработки данных. Каждый шаг pipeline — объект с методом process(data) -> data. Шаги:
Validator— принимаетrules(список функций-предикатов). Проверяет каждое правило, выбрасываетValueErrorпри нарушении.Transformer— принимаетtransform_fn(функция преобразования данных).Filter— принимаетpredicate(функция-предикат). Если данные — список, фильтрует его; иначе проверяет одиночный объект.Logger— выводит данные на каждом этапе, возвращает данные без изменений.
Напишите класс Pipeline, который принимает список шагов и метод run(data), последовательно применяющий их. Pipeline не должен проверять типы шагов — только вызывать step.process(data).
Добавьте LambdaStep — обёртку над функцией: LambdaStep(fn), process(data) просто вызывает fn(data).
Пример использования:
pipeline = Pipeline([
Logger("[Input]"),
Validator([
lambda d: len(d) > 0, # не пустой список
lambda d: all(isinstance(x, dict) for x in d) # список словарей
]),
Filter(lambda user: user.get("is_active", False)),
Transformer(lambda users: [u["email"] for u in users]),
LambdaStep(lambda emails: sorted(emails)),
Logger("[Output]"),
])
users = [
{"email": "charlie@example.com", "is_active": False},
{"email": "alice@example.com", "is_active": True},
{"email": "bob@example.com", "is_active": True},
]
result = pipeline.run(users)
print(result) # ['alice@example.com', 'bob@example.com']Диспетчер команд с isinstance()
Создайте систему обработки команд REST API. Базовый класс Command с атрибутами resource (строка) и payload (словарь).
Дочерние классы: CreateCommand, UpdateCommand, DeleteCommand, ReadCommand.
Класс CommandDispatcher содержит словарь {CommandClass: handler_function} и метод register(command_class, handler). Метод dispatch(command) использует isinstance() для нахождения подходящего обработчика — проверяет по иерархии (обработчик для базового класса вызывается для всех дочерних). Добавьте метод dispatch_all(commands) с подсчётом успешных и ошибочных обработок.
Пример использования:
dispatcher = CommandDispatcher()
dispatcher.register(ReadCommand, lambda c: print(f"READ {c.resource}"))
dispatcher.register(CreateCommand, lambda c: print(f"CREATE {c.resource}: {c.payload}"))
dispatcher.register(UpdateCommand, lambda c: print(f"UPDATE {c.resource}: {c.payload}"))
dispatcher.register(DeleteCommand, lambda c: print(f"DELETE {c.resource}"))
commands = [
ReadCommand("users"),
CreateCommand("users", {"name": "Alice"}),
UpdateCommand("users/1", {"name": "Bob"}),
DeleteCommand("users/2"),
]
stats = dispatcher.dispatch_all(commands)
print(stats) # {'success': 4, 'errors': 0}Протокол «экспортируемый»
Создайте ABC Exportable с методом to_export_dict() -> dict и __subclasshook__, который возвращает True для любого класса, имеющего метод to_export_dict.
Создайте три класса:
ProductModel(наследуетExportable) — атрибутыname,price,category. Реализуетto_export_dict().OrderModel(наследуетExportable) — атрибутыorder_id,items(список),total. Реализуетto_export_dict().LegacyReport(НЕ наследуетExportable) — старый класс с методомto_export_dict(). Должен пройти проверкуisinstance(..., Exportable)через__subclasshook__.
Напишите функцию export_batch(objects: list) -> list, которая:
- Принимает любые объекты.
- Проверяет через
isinstance(obj, Exportable)— если объект поддерживает протокол, экспортирует его. - Для объектов, не поддерживающих протокол — добавляет в результат
{"error": "not exportable", "type": type(obj).__name__}.
Пример использования:
objects = [
ProductModel("Ноутбук", 89990, "electronics"),
OrderModel(1001, ["item1", "item2"], 91490),
LegacyReport("Q1 2024", {"revenue": 1000000}),
"just a string", # не поддерживает протокол
42, # не поддерживает протокол
]
result = export_batch(objects)
for item in result:
print(item)