IO, базы и сериализация в Python
Формат хранения и загрузки решает и скорость, и корректность данных. Собес проверяет, знаешь ли ты, почему 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, остальные разбираются в тренажёре.
- Зачем в пайплайне используют пул соединений (connection pool) к БД?A)Пул нужен, чтобы каждый запрос наверняка открывал новое свежее соединение с нуляB)Пул соединений автоматически ускоряет выполнение самих SQL-запросов внутри базы данныхC)Пул полностью снимает лимиты на число одновременных подключений к серверу базы данныхD)Переиспользовать открытые соединения вместо дорогого открытия/закрытия на каждый запрос
показать ответ и разбор
+D)Переиспользовать открытые соединения вместо дорогого открытия/закрытия на каждый запрос// разбор: Открытие соединения к БД дорого: TCP-хендшейк, аутентификация, инициализация сессии. Пул держит набор готовых соединений и выдаёт их задачам, возвращая обратно после использования — цена коннекта платится раз, а не на каждый запрос. Пул также ограничивает число одновременных соединений (защита БД от исчерпания слотов). Без пула частые короткие запросы тратят время в основном на установку связи.
- Почему промежуточные данные пайплайна часто пишут в Parquet, а не в JSON/CSV?A)Колоночный сжатый бинарный формат: меньше объём, чтение нужных колонок, типы и pushdown — быстрее дальшеB)JSON компактнее Parquet, потому что текст сжимается лучше бинарного формата храненияC)Parquet требует читать весь файл целиком, тогда как из JSON можно взять одну колонкуD)Форматы равнозначны по объёму и скорости, выбор между ними — дело вкуса
показать ответ и разбор
+A)Колоночный сжатый бинарный формат: меньше объём, чтение нужных колонок, типы и pushdown — быстрее дальше// разбор: Parquet — колоночный бинарный формат со сжатием, схемой и статистикой: он в разы компактнее текстового JSON/CSV, хранит типы (не парсить строки), позволяет читать только нужные колонки и отсекать блоки по статистике (predicate pushdown). Для промежуточных слоёв и обмена между шагами это кратно дешевле по месту и I/O. JSON/CSV удобны как интерфейс с внешним миром, но не как рабочий формат больших данных.
- Зачем на границе пайплайна описывать записи через pydantic/dataclass со схемой?A)Чтобы ускорить сериализацию: модели pydantic работают быстрее обычных словарей PythonB)Типобезопасная валидация на границе, битое отсекается раноC)Чтобы отказаться от тестов пайплайна: схема заменяет собой проверку логики трансформацийD)Чтобы данные автоматически сохранялись в базу без написания какого-либо кода загрузки
показать ответ и разбор
+B)Типобезопасная валидация на границе, битое отсекается рано// разбор: Модель pydantic (или dataclass + валидация) на входе описывает ожидаемую схему: типы, обязательность, диапазоны. Данные проверяются один раз на границе — некорректные ловятся сразу с понятной ошибкой, а дальше по коду идут уже типизированные, валидные объекты, а не сырые dict с сюрпризами. Это сдвигает обнаружение проблем к источнику (fail fast) и документирует контракт данных.
- Файл JSON на 50 ГБ надо обработать на машине с 16 ГБ RAM. Как читать?A)Просто вызвать json.load() — Python сам обработает файл размера порциями под капотомB)Загрузить весь файл в один список и затем итерироваться по нему в цикле forC)Streaming-парсинг/NDJSON — по объекту за раз, память ограниченаD)Прочитать файл целиком в строку, а потом много раз разрезать её методом split
показать ответ и разбор
+C)Streaming-парсинг/NDJSON — по объекту за раз, память ограничена// разбор: json.load() строит всё дерево в памяти и на 50 ГБ падает. Решения: потоковый парсер (ijson) выдаёт элементы по мере разбора, не держа весь документ; или, если формат — newline-delimited JSON (по объекту на строку), читать файл построчно генератором и парсить каждую строку отдельно. В обоих случаях пик памяти ограничен одним объектом, а не файлом. NDJSON — стандарт для больших JSON-логов именно поэтому.
- Повторный запуск загрузки должен не задваивать строки. Что писать вместо голого 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 по партиции/окну перед вставкой. Ключ к безопасным ретраям и переналивкам.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.