Экспортёр Python оброс isinstance: расширяем через singledispatch
Функция экспорта принимает Decimal, дату, словарь и список. После каждого нового типа в неё добавляют ещё один elif isinstance. Через несколько изменений поддержка отчётов смешивается с устройством доменных объектов, а неизвестный тип случайно превращается в строку.
Отделить выбор обработчика от результата
Если правила зависят от типа одного значения, singledispatch позволяет зарегистрировать отдельные функции. Общая функция задаёт поведение для неизвестного объекта, специализированные обработчики — конкретные преобразования. При вызове выбирается реализация по типу первого аргумента. Это удобно для экспортного слоя, который знает о нескольких типах, но не должен менять их классы.
Сначала сформулируйте формат. Здесь результат должен состоять из JSON-совместимых значений. Decimal превращается в десятичную строку для сохранения представления. Дата становится строкой ISO. Список и словарь рекурсивно вызывают тот же экспортёр. Неизвестный объект вызывает TypeError: это сигнал добавить осмысленное правило, а не молча записать техническое строковое представление в контракт API.
Регистрация может учитывать наследование. Например, bool является подклассом int; если зарегистрировать только обработчик int, он способен получить и булево значение. Иногда это меняет смысл, например когда числовой обработчик приводит значение к строке. В примере bool зарегистрирован явно, чтобы сохранять JSON true/false. Обрабатывайте такие пересечения вместе с выбором формата, а не после неожиданного ответа клиенту.
Рекурсивный экспорт с явным отказом
У dict проверяются строковые ключи. JSON технически допускает поведение кодировщика, которое преобразует некоторые другие ключи, но такой неявный переход может смешать разные значения. Отказ на входе помогает сохранить стабильную схему. Для настоящего API этот контракт согласуйте с клиентом и схемой данных; singledispatch выбирает функцию, но не проектирует формат вместо разработчика.
Пример собирает заказ с суммой, датой и булевым признаком. Результат проходит через json.dumps, затем читается обратно. Проверка относится к фактическому сериализованному документу, поэтому ловит тип, который случайно остался непригодным для JSON. Отдельный вызов с object проверяет, что неизвестные значения не исчезают и не получают произвольное строковое представление.
from functools import singledispatch
from decimal import Decimal
from datetime import date
import json
@singledispatch
def export(value):
raise TypeError(f"unsupported type: {type(value).__name__}")
@export.register(str)
@export.register(int)
@export.register(bool)
@export.register(type(None))
def export_scalar(value):
return value
@export.register(Decimal)
def export_decimal(value):
return str(value)
@export.register(date)
def export_date(value):
return value.isoformat()
@export.register(list)
def export_list(value):
return [export(item) for item in value]
@export.register(dict)
def export_dict(value):
if not all(isinstance(key, str) for key in value):
raise TypeError("string keys required")
return {key: export(item) for key, item in value.items()}
order = {"total": Decimal("12.50"), "day": date(2026, 11, 3), "flags": [True, None]}
document = json.dumps(export(order), ensure_ascii=False)
assert json.loads(document) == {"total": "12.50", "day": "2026-11-03", "flags": [True, None]}
try:
export(object())
except TypeError:
pass
else:
raise AssertionError("unknown type accepted")
print(document)Граница удобного расширения
Новый обработчик регистрируйте в понятном модуле экспортного слоя. Разбросанная регистрация во время случайных импортов делает поведение зависимым от порядка запуска. Если используются плагины, загрузите их явно при старте. Две реализации для одного типа требуют согласованного владельца: последняя регистрация заменит предыдущую, а вызывающий код этого по сигнатуре не увидит.
Показанный формат принимает list, но не произвольный Iterable. Это осознанная граница: генератор может быть бесконечным или читать внешний ресурс, и экспорт незаметно запустит эту работу. Для datetime также понадобится отдельное правило часового пояса, поскольку это подкласс date. Не расширяйте поддержку типов лишь ради количества регистраций; каждый новый тип добавляет обещание формата.
Сохраните тесты на вложенность, bool, неизвестный объект и ключи словаря. Если типов мало и они больше не растут, обычная функция с несколькими ветками тоже читаема. singledispatch особенно полезен, когда экспорт развивается независимо от доменных классов и новые варианты нужно добавлять локально, сохраняя общую точку входа и единый контракт результата.
Перед подключением экспортёра к ответу сервиса прогоните через него реальные формы данных: вложенный пустой список, пустой словарь, нулевую сумму и отсутствующее значение. Такие входы проверяют сохранение структуры, а не только выбор обработчика. Если приходится учитывать одновременно тип значения и целевой формат, один singledispatch уже может быть недостаточен: правила JSON и табличного отчёта лучше оформить отдельными точками входа. Иначе одна регистрация начнёт менять поведение сразу нескольких потребителей. Сохраняйте небольшой публичный API преобразования и отдельные тесты контракта для каждого формата. Тогда подключение нового доменного класса останется локальным расширением экспортного слоя, а не изменением всех мест, которые когда-либо использовали строковое представление этого объекта.
Следующий шаг в практике
Откройте бесплатный урок о специальных методах объектов, чтобы отделить строковое представление для человека от формата обмена. В программе продвинутого Python продолжите полиморфизмом и интерфейсами. «Python для профессионалов» предлагает дальнейшую практику декораторов и продвинутой стандартной библиотеки.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
