сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · ETL и оркестрация

IO, базы и сериализация в Python

I/O и сериализация

Формат хранения и загрузки решает и скорость, и корректность данных. Собес проверяет, знаешь ли ты, почему parquet - дефолт, чем коварен CSV и почему pickle нельзя от чужих.

Стержень: parquet для данных, CSV только как интерфейс с внешним миром, pickle - только внутренний и доверенный.

// Формулировки: «в каком формате хранить данные?», «чем опасен CSV?», «почему pickle небезопасен?».

Parquet, CSV, pickle

Parquet - дефолт для данных: колоночный, типизированный, сжатый, с предикат-пушдауном (фильтр применяется при чтении). CSV годится только как интерфейс с внешним миром, не для хранения.

CSV коварен потерей типов: даты и айди становятся строками или float, всплывают кодировки, разделители внутри значений, ведущие нули. Читать CSV нужно с явными dtype и кодировкой, иначе айди 007 станет числом 7, а дата - строкой.

// pickle - только внутренний и доверенный: pickle.load файла из внешнего источника выполняет произвольный код (RCE - remote code execution), а формат ещё и зависит от версий библиотек. Для обмена - parquet или json, не pickle.

columnar формат
хранение по колонкам: читаешь только нужные
pickle-RCE
pickle.load недоверенного файла выполняет чужой код

JSON и кодировки

json.dumps из коробки не умеет datetime, Decimal, ndarray - на них он бросает TypeError. Нужен default= или кастомный энкодер. Для потока больших JSON берут jsonlines (объект на строку), а не один гигантский массив, который нельзя читать потоково.

Кодировки задают явно: encoding='utf-8' при открытии файлов. Данные из Windows-мира часто в cp1251.

// Характерный симптом «Ð¿Ñ€Ð¸Ð²ÐµÑ‚» вместо «привет» это не битые данные, а перепутанные кодировки: файл в одной, читаешь как другую. Чтение без явного encoding упадёт не сегодня, а на первом клиенте с cp1251.

# json не умеет datetime сам:
json.dumps(dt)              # TypeError
json.dumps(dt, default=str) # ок
jsonlines
JSON-объект на строку - потоковое чтение/запись
cp1251
windows-кодировка; источник «кракозябр» при чтении как utf-8

БД, сжатие, схема

С БД: для больших выборок - курсор с fetchmany или серверный курсор, для массовых вставок - executemany или COPY. INSERT в цикле по одной строке на миллионе записей идёт часы вместо секунд.

Сжатие - трейдоф CPU против размера: snappy и zstd быстрые для горячих данных, gzip плотнее для холодных; parquet сжимает по колонкам сам.

// Схему фиксируют явно: dtype при чтении, схема parquet, pydantic для API-данных. «pandas сам догадается» ломается на пустых колонках и краях - на пустой колонке тип уедет, и следующий шаг упадёт.

COPY / bulk insert
массовая загрузка в БД вместо построчных INSERT
zstd / snappy
современные быстрые кодеки сжатия

Как отвечать: «Почему parquet, а не CSV или pickle для данных?»

Потому что у каждого своя роль, и для хранения данных выигрывает parquet. Он колоночный, поэтому читает только нужные колонки и фильтрует по статистикам при чтении; он типизированный, поэтому не теряет типы; и сжат по колонкам. CSV всего этого лишён и вдобавок коварен: он не хранит типы, поэтому на round-trip айди с ведущими нулями вроде 007 превращается в число 7, дата - в строку, всплывают кодировки и разделители внутри значений. Поэтому CSV я держу только как интерфейс с внешним миром и читаю с явными dtype и encoding. pickle вообще не для обмена: его загрузка из недоверенного источника выполняет произвольный код, это готовый RCE, и формат ещё и завязан на версии библиотек. Так что pickle - только внутренний кэш между доверенными процессами, а для данных и обмена - parquet, в крайнем случае json.

Почему это сильный ответ: три формата с чёткими ролями, конкретный вред CSV (потеря типов, 007→7) и главная опасность pickle (RCE) - не «parquet быстрее», а понимание, что каждый формат решает.

На чём валят

  • pickle.load файла из внешнего источника - выполнение чужого кода.
  • CSV round-trip теряет типы: айди 007 стал 7, дата стала строкой.
  • INSERT в цикле по одной строке на миллионе записей - часы вместо секунд.
  • json.dumps(datetime) - TypeError в проде, если не покрыт тестом.
  • Чтение без encoding на «странном» файле - упадёт не сегодня, а на первом клиенте с cp1251.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 13, остальные разбираются в тренажёре.

  1. #io_serialization1 / 5
    Зачем в пайплайне используют пул соединений (connection pool) к БД?
    A)Пул нужен, чтобы каждый запрос наверняка открывал новое свежее соединение с нуля
    B)Пул соединений автоматически ускоряет выполнение самих SQL-запросов внутри базы данных
    C)Пул полностью снимает лимиты на число одновременных подключений к серверу базы данных
    D)Переиспользовать открытые соединения вместо дорогого открытия/закрытия на каждый запрос
    показать ответ и разбор
    +D)Переиспользовать открытые соединения вместо дорогого открытия/закрытия на каждый запрос

    // разбор: Открытие соединения к БД дорого: TCP-хендшейк, аутентификация, инициализация сессии. Пул держит набор готовых соединений и выдаёт их задачам, возвращая обратно после использования — цена коннекта платится раз, а не на каждый запрос. Пул также ограничивает число одновременных соединений (защита БД от исчерпания слотов). Без пула частые короткие запросы тратят время в основном на установку связи.

  2. #io_serialization2 / 5
    Почему промежуточные данные пайплайна часто пишут в Parquet, а не в JSON/CSV?
    A)Колоночный сжатый бинарный формат: меньше объём, чтение нужных колонок, типы и pushdown — быстрее дальше
    B)JSON компактнее Parquet, потому что текст сжимается лучше бинарного формата хранения
    C)Parquet требует читать весь файл целиком, тогда как из JSON можно взять одну колонку
    D)Форматы равнозначны по объёму и скорости, выбор между ними — дело вкуса
    показать ответ и разбор
    +A)Колоночный сжатый бинарный формат: меньше объём, чтение нужных колонок, типы и pushdown — быстрее дальше

    // разбор: Parquet — колоночный бинарный формат со сжатием, схемой и статистикой: он в разы компактнее текстового JSON/CSV, хранит типы (не парсить строки), позволяет читать только нужные колонки и отсекать блоки по статистике (predicate pushdown). Для промежуточных слоёв и обмена между шагами это кратно дешевле по месту и I/O. JSON/CSV удобны как интерфейс с внешним миром, но не как рабочий формат больших данных.

  3. #io_serialization3 / 5
    Зачем на границе пайплайна описывать записи через pydantic/dataclass со схемой?
    A)Чтобы ускорить сериализацию: модели pydantic работают быстрее обычных словарей Python
    B)Типобезопасная валидация на границе, битое отсекается рано
    C)Чтобы отказаться от тестов пайплайна: схема заменяет собой проверку логики трансформаций
    D)Чтобы данные автоматически сохранялись в базу без написания какого-либо кода загрузки
    показать ответ и разбор
    +B)Типобезопасная валидация на границе, битое отсекается рано

    // разбор: Модель pydantic (или dataclass + валидация) на входе описывает ожидаемую схему: типы, обязательность, диапазоны. Данные проверяются один раз на границе — некорректные ловятся сразу с понятной ошибкой, а дальше по коду идут уже типизированные, валидные объекты, а не сырые dict с сюрпризами. Это сдвигает обнаружение проблем к источнику (fail fast) и документирует контракт данных.

  4. #io_serialization4 / 5
    Файл JSON на 50 ГБ надо обработать на машине с 16 ГБ RAM. Как читать?
    A)Просто вызвать json.load() — Python сам обработает файл размера порциями под капотом
    B)Загрузить весь файл в один список и затем итерироваться по нему в цикле for
    C)Streaming-парсинг/NDJSON — по объекту за раз, память ограничена
    D)Прочитать файл целиком в строку, а потом много раз разрезать её методом split
    показать ответ и разбор
    +C)Streaming-парсинг/NDJSON — по объекту за раз, память ограничена

    // разбор: json.load() строит всё дерево в памяти и на 50 ГБ падает. Решения: потоковый парсер (ijson) выдаёт элементы по мере разбора, не держа весь документ; или, если формат — newline-delimited JSON (по объекту на строку), читать файл построчно генератором и парсить каждую строку отдельно. В обоих случаях пик памяти ограничен одним объектом, а не файлом. NDJSON — стандарт для больших JSON-логов именно поэтому.

  5. #io_serialization5 / 5
    Повторный запуск загрузки должен не задваивать строки. Что писать вместо голого INSERT?
    A)Просто добавить SELECT перед каждым INSERT и проверить наличие строки в приложении вручную
    B)Ничего: повторный INSERT тех же данных база сама молча проигнорирует без всяких дубликатов
    C)Отключить уникальные ограничения таблицы, тогда дубликаты перестанут появляться при повторе
    D)Идемпотентный UPSERT (INSERT ... ON CONFLICT DO UPDATE) по ключу — повтор обновляет, а не дублирует
    показать ответ и разбор
    +D)Идемпотентный UPSERT (INSERT ... ON CONFLICT DO UPDATE) по ключу — повтор обновляет, а не дублирует

    // разбор: Голый INSERT при повторном запуске (ретрай, backfill) создаёт дубликаты. UPSERT — INSERT ... ON CONFLICT (Postgres) / MERGE — по уникальному ключу вставляет новое и обновляет существующее, поэтому повторная загрузка тех же данных приводит систему в то же состояние (идемпотентность). Альтернатива — delete-insert по партиции/окну перед вставкой. Ключ к безопасным ретраям и переналивкам.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.