Новинка: «Инженерия данных. Паттерны проектирования»
Издательство «O’Reilly» выпустило русское издание книги «Data Engineering Design Patterns» Бартоша Конечны под названием «Инженерия данных. Паттерны проектирования». Книга вышла в конце июня 2026 года. Автор предлагает выделить в дисциплине инженерии данных универсальные шаблоны проектирования типичных решений, аналогичные тем, что описаны в книге «Design Patterns» «Банды четырёх» середины 1990-х. Ранее Бартош Конечны опубликовал статью-перевод, в которой обосновывал готовящуюся книгу и очерчивал её тематическое поле.
Читать далее
Издательство «O’Reilly» выпустило русское издание книги «Data Engineering Design Patterns» Бартоша Конечны под названием «Инженерия данных. Паттерны проектирования». Книга вышла в конце июня 2026 года. Автор предлагает выделить в дисциплине инженерии данных универсальные шаблоны проектирования типичных решений, аналогичные тем, что описаны в книге «Design Patterns» «Банды четырёх» середины 1990-х. Ранее Бартош Конечны опубликовал статью-перевод, в которой обосновывал готовящуюся книгу и очерчивал её тематическое поле.
Читать далее
Lock‑Free кэш на C11 atomics через Python – когда GIL не помогает, а блокировки давят
В многопроцессной архитектуре (например, между воркерами Gunicorn или Celery) обычные Lock из threading или multiprocessing становятся узким местом из‑за контекстных переключений. Lock‑free структуры на атомарных операциях C11 обходят это. Python позволяет дотянуться до них через
Как собирается атомарный кэш
Берёшь
Production‑ориентированный пример
Используй такой кэш на hot‑path, где простая блокичка через
Trade‑offs и типичные ошибки
Плюсы: без блокировок, масштабируется на многоядерных системах, GIL не мешает. Минусы: ABA‑проблема (например, чтение старой записи после перезаписи), платформозависимость (не на всех архитектурах есть CAS), риск data race при неправильном memory ordering. Предупреждение: не используй этот подход для сложных структур – lock‑free очередь или счётчик проще сделать через
Вывод: Lock‑free кэш на атомарных операциях оправдан только на узком hot‑path, где блокировка реально давит – в остальных случаях простой Lock надёжнее и читаемее.
В многопроцессной архитектуре (например, между воркерами Gunicorn или Celery) обычные Lock из threading или multiprocessing становятся узким местом из‑за контекстных переключений. Lock‑free структуры на атомарных операциях C11 обходят это. Python позволяет дотянуться до них через
ctypes и _multiprocessing.sharedctypes, работая напрямую с разделяемой памятью без GIL. Частая ошибка: разработчики пишут свой lock‑free код, не учитывая memory ordering и ABA‑проблему.Как собирается атомарный кэш
Берёшь
RawValue и RawArray из _multiprocessing.sharedctypes, выделяешь разделяемую память для флагов, ключей и значений. Для синхронизации используешь C11 __sync_bool_compare_and_swap (CAS) через ctypes.CFUNCTYPE – он вызывает atomic-инструкцию на уровне процессора.from ctypes import c_uint64, c_bool, CFUNCTYPE, POINTER, byref
from _multiprocessing.sharedctypes import RawValue, RawArray
class LockFreeCache:
def __init__(self, capacity=256):
self.capacity = capacity
self.keys = RawArray(c_uint64, capacity)
self.values = RawArray(c_uint64, capacity)
self.flags = RawArray(c_bool, capacity)
self._load_cas()
def _load_cas(self):
libc = ctypes.CDLL(None)
self.cas = CFUNCTYPE(c_bool, POINTER(c_bool), c_bool, c_bool)(
('__sync_bool_compare_and_swap', libc)
)
def set(self, key, value):
idx = hash(key) % self.capacity
while True:
old_flag = c_bool(False)
if self.cas(byref(self.flags[idx]), old_flag, c_bool(True)):
self.keys[idx] = key
self.values[idx] = value
self.flags[idx] = c_bool(False)
return True
Production‑ориентированный пример
Используй такой кэш на hot‑path, где простая блокичка через
multiprocessing.Lock даёт ощутимый оверхед. Например, подсчёт запросов или кэширование результатов между воркерами в асинхронном HTTP‑обработчике. Для прода обязательно добавь управление коллизиями (хэш‑таблица с open addressing), TTL и видимость через memory fence (GCC __sync_synchronize).Trade‑offs и типичные ошибки
Плюсы: без блокировок, масштабируется на многоядерных системах, GIL не мешает. Минусы: ABA‑проблема (например, чтение старой записи после перезаписи), платформозависимость (не на всех архитектурах есть CAS), риск data race при неправильном memory ordering. Предупреждение: не используй этот подход для сложных структур – lock‑free очередь или счётчик проще сделать через
multiprocessing.Value с блокировкой.Вывод: Lock‑free кэш на атомарных операциях оправдан только на узком hot‑path, где блокировка реально давит – в остальных случаях простой Lock надёжнее и читаемее.
Геостатистика в QGIS без SAGA: кригинг на чистом NumPy
Автор статьи делится опытом создания софта для горно-геологических служб калийных рудников. Геологи и маркшейдеры ежедневно превращают тысячи скважинных проб в карты: отметки кровли пласта, содержания KCl, мощности, газоопасность. Классический инструмент для этого — кригинг, который в QGIS формально есть через SAGA, GRASS, Smart-Map и связки со SciPy. Однако каждый из этих вариантов чем-то не устраивал, и год назад автор начал писать свой плагин.
Сейчас плагин Isoliner включает 24 инструмента в официальном репозитории plugins.qgis.org: кригинг четырёх видов, вариограммный анализ, кросс-валидация с отчётами, изолинии с контурными полигонами, геологические разрезы и собственный 3D-просмотр. Вычислительное ядро построено на чистом NumPy без внешних зависимостей.
В статье также объясняется, зачем понадобился ещё один кригинг, как выглядит система кригинга в двадцати строках NumPy, что такое вариограмма на пальцах и почему абсолютные единицы силла — главные грабли для новичков.
Читать далее на Хабре
Автор статьи делится опытом создания софта для горно-геологических служб калийных рудников. Геологи и маркшейдеры ежедневно превращают тысячи скважинных проб в карты: отметки кровли пласта, содержания KCl, мощности, газоопасность. Классический инструмент для этого — кригинг, который в QGIS формально есть через SAGA, GRASS, Smart-Map и связки со SciPy. Однако каждый из этих вариантов чем-то не устраивал, и год назад автор начал писать свой плагин.
Сейчас плагин Isoliner включает 24 инструмента в официальном репозитории plugins.qgis.org: кригинг четырёх видов, вариограммный анализ, кросс-валидация с отчётами, изолинии с контурными полигонами, геологические разрезы и собственный 3D-просмотр. Вычислительное ядро построено на чистом NumPy без внешних зависимостей.
В статье также объясняется, зачем понадобился ещё один кригинг, как выглядит система кригинга в двадцати строках NumPy, что такое вариограмма на пальцах и почему абсолютные единицы силла — главные грабли для новичков.
Читать далее на Хабре
Сборка колл-стеков для профилирования CPU в production через sys.setprofile и signal.setitimer без внешних трейсеров
Когда прожорливый код угробит CPU в продакшене, а ставить py-spy или Pyroscope нельзя из-за политики безопасности или ограничений архитектуры, поможет связка стандартной библиотеки. Разработчики часто забывают, что семплер можно собрать за час без единой внешней зависимости, но допускают ошибки с потоками и задержками.
Идея и реализация
Production-ready пример для мониторинга CPU-горячих точек
Типичные ошибки и trade-offs
* Сигнал SIGALRM работает только в главном потоке — для многопоточности нужен ручной
* Минимальная частота — 50 мс; ниже уже жрёт CPU сам семплер. Для asyncio проще использовать
* C-расширения (например, numpy или lxml) не покажут стек — тут без py-spy или perf не обойтись.
Практический совет
Записывай в кольцевой буфер с фиксированной ёмкостью (например, 1000 сэмплов) и асинхронно сбрасывай на диск в фоновом потоке — это thread-safe и не фризит основной код.
Вывод: Связка
Когда прожорливый код угробит CPU в продакшене, а ставить py-spy или Pyroscope нельзя из-за политики безопасности или ограничений архитектуры, поможет связка стандартной библиотеки. Разработчики часто забывают, что семплер можно собрать за час без единой внешней зависимости, но допускают ошибки с потоками и задержками.
Идея и реализация
signal.setitimer запускает обработчик через заданные интервалы (например, 50-100 мс). Внутри него sys._current_frames() делает мгновенный снапшот стеков всех потоков. Ключевой нюанс: не используй traceback.format_stack() — это тормозит. Только сухие f_code.co_name и f_lineno.Production-ready пример для мониторинга CPU-горячих точек
import signal
import sys
import threading
import time
stacks_buffer = []
BUFFER_LOCK = threading.Lock()
def collect_stack(signum, frame):
try:
snapshot = {}
for tid, t_frame in sys._current_frames().items():
stack = []
while t_frame:
stack.append(f"{t_frame.f_code.co_name}:{t_frame.f_lineno}")
t_frame = t_frame.f_back
snapshot[tid] = stack[:15] # Ограничиваем глубину
with BUFFER_LOCK:
stacks_buffer.append((time.time(), snapshot))
except Exception:
pass # Тихий сброс ошибок
signal.signal(signal.SIGALRM, collect_stack)
signal.setitimer(signal.ITIMER_REAL, 0.05, 0.05) # 50 ms
# Далее обычный код приложения...
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
signal.setitimer(signal.ITIMER_REAL, 0, 0)
for ts, snap in stacks_buffer[:5]:
print(ts, snap)
Типичные ошибки и trade-offs
* Сигнал SIGALRM работает только в главном потоке — для многопоточности нужен ручной
signal.signal и отдельный поток-коллектор для записи в буфер (иначе потеря данных из-за рекурсии сигналов).* Минимальная частота — 50 мс; ниже уже жрёт CPU сам семплер. Для asyncio проще использовать
asyncio.Task.get_stack() из цикла событий.* C-расширения (например, numpy или lxml) не покажут стек — тут без py-spy или perf не обойтись.
Практический совет
Записывай в кольцевой буфер с фиксированной ёмкостью (например, 1000 сэмплов) и асинхронно сбрасывай на диск в фоновом потоке — это thread-safe и не фризит основной код.
Вывод: Связка
sys.setprofile + signal.setitimer даёт дешёвый семплинг стека для поиска CPU-горячих точек в продакшене без внешних зависимостей, но требует аккуратности с потоками, частотой и обработкой C-расширений.Типизированные TypeVar и ParamSpec для generic-коллбэков: статическая проверка обработчиков событий без Protocol и лишних абстракций
Типизация коллбэков — вечная боль production-кода, особенно при реализации диспетчеров событий или callback-based middleware. Частая ошибка: использовать
Проблема
Здесь анализатор не проверяет аргументы. В production это приводит к ошибкам при первом несовпадении типов — например, когда обработчик ожидает
Решение
Статический анализатор теперь видит точную сигнатуру коллбэка. Никаких сюрпризов.
Реальный пример: диспетчер событий
Почему это лучше Protocol?
* Меньше кода — не надо объявлять абстрактные классы с
* Точность — сохраняются имена параметров, порядок, типы и возвращаемое значение
* Универсальность — один декоратор работает с любыми сигнатурами
* Совместимость — работает с callback-based либами вроде
Когда Protocol все же нужен?
Если коллбэк должен иметь атрибуты (например,
Вывод: ParamSpec + TypeVar дают чистую и проверяемую типизацию generic-коллбэков без лишних сущностей — замените
Типизация коллбэков — вечная боль production-кода, особенно при реализации диспетчеров событий или callback-based middleware. Частая ошибка: использовать
Callable[..., Any], теряя сигнатуру и получая TypeError на проде.Проблема
def register(handler: Callable[..., Any]):
...
Здесь анализатор не проверяет аргументы. В production это приводит к ошибкам при первом несовпадении типов — например, когда обработчик ожидает
(int, str), а передается (float, User).Решение
from typing import TypeVar, ParamSpec, Callable
P = ParamSpec("P")
T = TypeVar("T")
def register(
handler: Callable[P, T],
*args: P.args,
**kwargs: P.kwargs,
) -> T:
return handler(*args, **kwargs)
Статический анализатор теперь видит точную сигнатуру коллбэка. Никаких сюрпризов.
Реальный пример: диспетчер событий
class EventDispatcher:
def on(self, event: str) -> Callable[[Callable[P, T]], Callable[P, T]]:
def wrapper(handler: Callable[P, T]) -> Callable[P, T]:
self.handlers[event] = handler
return handler
return wrapper
dispatcher = EventDispatcher()
@dispatcher.on("user_login")
def handle_login(user_id: int, timestamp: float) -> str:
return f"User {user_id} logged in at {timestamp}"
# Правильно
result = dispatcher.handlers["user_login"](42, 1689000000.0)
# Ошибка типов: expected float, got str
result = dispatcher.handlers["user_login"](42, "bad")
Почему это лучше Protocol?
* Меньше кода — не надо объявлять абстрактные классы с
__call__* Точность — сохраняются имена параметров, порядок, типы и возвращаемое значение
* Универсальность — один декоратор работает с любыми сигнатурами
* Совместимость — работает с callback-based либами вроде
functools или asyncioКогда Protocol все же нужен?
Если коллбэк должен иметь атрибуты (например,
handler.priority = 5) или наследовать несколько абстракций. По опыту — в 80% случаев это избыточно.Вывод: ParamSpec + TypeVar дают чистую и проверяемую типизацию generic-коллбэков без лишних сущностей — замените
Callable[..., Any] на точные сигнатуры на ревью.❤1
Graceful Shutdown в asyncio: как не потерять данные при SIGTERM в production
Стандартный
Проблема: почему просто cancel() не работает
Сигналы обрабатываются в том же потоке, что и цикл событий. Если в обработчике сразу отменять корутины, вы получите состояние гонки: одни задачи завершатся, другие - нет. Ресурсы утекут, соединения повиснут. Пример частой ошибки:
Решение: pipe как мост между синхронным и асинхронным миром
Используйте
Критичное правило: никакой логики в _signal_handler
Запись в pipe - единственная операция. Закрытие дескрипторов, отмена задач,
Trade-off: pipe vs call_soon_threadsafe
Вывод: Pipe +
Стандартный
signal.signal блокирует event loop, превращая асинхронное приложение в синхронный ступор. Решение - loop.add_signal_handler(), но без pipe вы рискуете утечкой ресурсов: воркеры не успеют закрыть соединения или снять блокировки.Проблема: почему просто cancel() не работает
Сигналы обрабатываются в том же потоке, что и цикл событий. Если в обработчике сразу отменять корутины, вы получите состояние гонки: одни задачи завершатся, другие - нет. Ресурсы утекут, соединения повиснут. Пример частой ошибки:
def handler():
for task in asyncio.all_tasks():
task.cancel() # Блокировка в синхронном контексте
Решение: pipe как мост между синхронным и асинхронным миром
Используйте
os.pipe() для безусловного пробуждения цикла. Обработчик сигнала только пишет байт в pipe - никаких корутин или блокировок. Через add_reader асинхронный контекст подхватывает запись и выполняет graceful shutdown:class GracefulShutdown:
def __init__(self):
self.loop = asyncio.get_event_loop()
self.r_fd, self.w_fd = os.pipe()
self.loop.add_reader(self.r_fd, self._handle_stop)
for sig in (signal.SIGTERM, signal.SIGINT):
self.loop.add_signal_handler(sig, self._signal_handler)
def _signal_handler(self):
os.write(self.w_fd, b'\x00') # Только запись
Критичное правило: никакой логики в _signal_handler
Запись в pipe - единственная операция. Закрытие дескрипторов, отмена задач,
asyncio.all_tasks() - все это делается в асинхронном _handle_stop, где task.cancel() пробрасывает CancelledError в воркеры, давая шанс выполнить cleanup. Если нарушить это правило в многопоточном run_in_executor, словите блокировку цикла.Trade-off: pipe vs call_soon_threadsafe
loop.call_soon_threadsafe тоже работает, но pipe надежнее в сценариях с воркерами в других потоках: он гарантированно будит именно тот цикл, который слушает. Без pipe вы рискуете, что сигнал пропустит цикл, занятый долгим await.Вывод: Pipe +
add_signal_handler превращает SIGTERM из источника утечек в управляемое завершение, где каждый воркер закрывает ресурсы через CancelledError.Какой кригинг выбрать: простой, ординарный, с трендом, блочный, индикаторный
Мы создаем софт для горно-геологических служб калийных рудников, и после первой статьи про кригинг на чистом NumPy самый частый вопрос звучал одинаково: «Хорошо, а какой именно кригинг брать?» Вопрос правильный: под словом «кригинг» живёт целое семейство методов, и выбор между ними влияет на результат сильнее, чем тонкая настройка вариограммы.
В плагине Isoliner их пять - простой, ординарный, с полиномиальным трендом, блочный и индикаторный, - и каждый существует не для галочки, а под конкретный класс геологических задач. Все пять видов кригинга решают систему: оценка в точке — взвешенная сумма соседних скважин, веса — решение системы уравнений с ковариациями из вариограммы. Различаются они тем, что считается неизвестным про среднее поле и что именно оценивается — точка, блок или вероятность. Простой кригинг предполагает, что среднее значение поля известно заранее и постоянно по площади, и недобор веса соседей компенсируется этим средним.
Читать далее
Мы создаем софт для горно-геологических служб калийных рудников, и после первой статьи про кригинг на чистом NumPy самый частый вопрос звучал одинаково: «Хорошо, а какой именно кригинг брать?» Вопрос правильный: под словом «кригинг» живёт целое семейство методов, и выбор между ними влияет на результат сильнее, чем тонкая настройка вариограммы.
В плагине Isoliner их пять - простой, ординарный, с полиномиальным трендом, блочный и индикаторный, - и каждый существует не для галочки, а под конкретный класс геологических задач. Все пять видов кригинга решают систему: оценка в точке — взвешенная сумма соседних скважин, веса — решение системы уравнений с ковариациями из вариограммы. Различаются они тем, что считается неизвестным про среднее поле и что именно оценивается — точка, блок или вероятность. Простой кригинг предполагает, что среднее значение поля известно заранее и постоянно по площади, и недобор веса соседей компенсируется этим средним.
Читать далее
Zero-Copy Обёртки Над Буферами: Парсинг Бинарных Протоколов Через __buffer__ и __release_buffer__
Каждый раз, когда дамп сырого memoryview из сокета копируется в bytes для парсинга struct, теряется время — на production с десятками тысяч пакетов в секунду это становится узким местом. В Python 3.12+ протокол буфера позволяет реализовать zero-copy классы-обёртки без копирования данных.
Зачем нужны типизированные обёртки
Сырой memoryview — это всего лишь как бы массив байтов, где нужно помнить смещения и форматы. Класс с полями даёт типобезопасность и читаемость, при этом внутри работает с тем же буфером. Например, для фиксированного заголовка пакета: 4 байта ID, 2 байта длины, 2 байта флагов.
Zero-copy парсинг в действии
Вместо копирования через
Нет ни одного лишнего копирования. Это особенно критично для high-throughput протоколов в связке с asyncio, где каждый микросекунда на аллокацию — потеря.
Типичная ошибка и trade-offs
Управление временем жизни буфера — это головная боль. Если исходный memoryview будет уничтожен раньше обёртки, вы получите segfault или мусорные данные. Всегда явно контролируйте scope: держите ссылку на исходный буфер до завершения парсинга.
Вывод: Протокол
Каждый раз, когда дамп сырого memoryview из сокета копируется в bytes для парсинга struct, теряется время — на production с десятками тысяч пакетов в секунду это становится узким местом. В Python 3.12+ протокол буфера позволяет реализовать zero-copy классы-обёртки без копирования данных.
Зачем нужны типизированные обёртки
Сырой memoryview — это всего лишь как бы массив байтов, где нужно помнить смещения и форматы. Класс с полями даёт типобезопасность и читаемость, при этом внутри работает с тем же буфером. Например, для фиксированного заголовка пакета: 4 байта ID, 2 байта длины, 2 байта флагов.
import struct
class PacketHeader:
def __init__(self, buffer):
self._buffer = buffer
def __buffer__(self, flags):
return self._buffer.__buffer__(flags)
def __release_buffer__(self, buffer):
pass
@property
def packet_id(self):
return struct.unpack_from('<I', self._buffer, 0)[0]
@property
def length(self):
return struct.unpack_from('<H', self._buffer, 4)[0]
Zero-copy парсинг в действии
Вместо копирования через
bytes(data) используем MSG_PEEK и передаём memoryview напрямую:raw_data = memoryview(sock.recv(8, socket.MSG_PEEK))
header = PacketHeader(raw_data)
print(header.packet_id, header.length)
Нет ни одного лишнего копирования. Это особенно критично для high-throughput протоколов в связке с asyncio, где каждый микросекунда на аллокацию — потеря.
Типичная ошибка и trade-offs
Управление временем жизни буфера — это головная боль. Если исходный memoryview будет уничтожен раньше обёртки, вы получите segfault или мусорные данные. Всегда явно контролируйте scope: держите ссылку на исходный буфер до завершения парсинга.
Вывод: Протокол
__buffer__ и __release_buffer__ дают zero-copy доступ к бинарным данным без копирования, но требуют аккуратного менеджмента памяти и не заменяют быстрый struct.unpack для разовых задач.💡 CSV-отчёты с параметрами и без таймаута: быль из BI
Один чувак рассказал, как он вкалывал в BI крупного банка. Начальство тащилось от дашбордов и красивых графиков, а вот тем, кто реально возился с данными, приходилось лезть в выгрузки и разбираться, что там пошло не так — баг ли, задержка загрузки, или формула мудрит.
У финансистов не было доступа к базе, поэтому они таскали данные в Excel, сравнивали выгрузки и вручную проверяли метрики. Под них замутили первую версию софта: он клепал отчёты в Excel и сохранял историю выгрузок на сервере.
Читать далее
👉 About Python
Один чувак рассказал, как он вкалывал в BI крупного банка. Начальство тащилось от дашбордов и красивых графиков, а вот тем, кто реально возился с данными, приходилось лезть в выгрузки и разбираться, что там пошло не так — баг ли, задержка загрузки, или формула мудрит.
У финансистов не было доступа к базе, поэтому они таскали данные в Excel, сравнивали выгрузки и вручную проверяли метрики. Под них замутили первую версию софта: он клепал отчёты в Excel и сохранял историю выгрузок на сервере.
Читать далее
Please open Telegram to view this post
VIEW IN TELEGRAM
Zero-downtime миграции схем в SQLAlchemy 2.0: партиционирование и transactional DDL в Alembic
Когда таблица переваливает за 100 миллионов строк, обычная миграция через ALTER TABLE превращается в план на выходные с даунтаймом. Если повезёт.
Почему transactional DDL критичен
PostgreSQL умеет выполнять DDL внутри транзакций — это transactional DDL. Alembic это поддерживает, но при партиционировании важно не забыть про
Expand-contract стратегия
Вот как выглядит zero-downtime подход:
Фаза 1 (Expand) — создаём новую партиционированную таблицу параллельно старой. Не трогаем существующую схему.
Фаза 2 (Migrate) — организуем двойную запись. Апдейты пишутся и в старую, и в новую структуру. Через триггеры или приложение.
Фаза 3 (Contract) — переключаем чтение на новую схему. Удаляем старую таблицу, когда убедились, что всё работает.
В коде Alembic это выглядит примерно так:
Главные грабли
* Deferred constraints. Если есть внешние ключи, их лучше отключать на время миграции, иначе потом не запихнете данные.
* Batch processing. Не пытайтесь перелить 100M строк одним INSERT. Делайте чанками по 1000 записей с
* Versioned migrations. Сохраняйте старые партиции хотя бы неделю после переключения. На проде всегда вылезает "ой, а мы забыли перенести поле".
Production-oriented пример
Создаём теневую таблицу, льём данные батчами, потом атомарно переименовываем:
Предостережения, которые обычно игнорируют
* Тестируйте на клоне прода. Не на тестовой базе с 10 строками.
* Используйте
* Мониторьте
* Имейте rollback-скрипт для каждой фазы. Если что-то пошло на этапе переименования, откат должен быть за секунды.
Вывод: Партиционирование через transactional DDL — это не магия, а расчёт, который окупается, когда таблицы растут на 10% в месяц, иначе проще оставить как есть.
Когда таблица переваливает за 100 миллионов строк, обычная миграция через ALTER TABLE превращается в план на выходные с даунтаймом. Если повезёт.
Почему transactional DDL критичен
PostgreSQL умеет выполнять DDL внутри транзакций — это transactional DDL. Alembic это поддерживает, но при партиционировании важно не забыть про
autocommit_block(). Без него некоторые операции (создание партиций) могут упасть вне транзакции, и сервер уйдёт в простой.Expand-contract стратегия
Вот как выглядит zero-downtime подход:
Фаза 1 (Expand) — создаём новую партиционированную таблицу параллельно старой. Не трогаем существующую схему.
Фаза 2 (Migrate) — организуем двойную запись. Апдейты пишутся и в старую, и в новую структуру. Через триггеры или приложение.
Фаза 3 (Contract) — переключаем чтение на новую схему. Удаляем старую таблицу, когда убедились, что всё работает.
В коде Alembic это выглядит примерно так:
def upgrade():
with op.get_context().autocommit_block():
op.execute("""
CREATE TABLE orders_new (LIKE orders INCLUDING ALL)
PARTITION BY RANGE (created_at)
""")
# Дальше — батчевая вставка данных с паузами
Главные грабли
* Deferred constraints. Если есть внешние ключи, их лучше отключать на время миграции, иначе потом не запихнете данные.
* Batch processing. Не пытайтесь перелить 100M строк одним INSERT. Делайте чанками по 1000 записей с
time.sleep(0.1). Иначе транзакция отожмёт всё.* Versioned migrations. Сохраняйте старые партиции хотя бы неделю после переключения. На проде всегда вылезает "ой, а мы забыли перенести поле".
Production-oriented пример
Создаём теневую таблицу, льём данные батчами, потом атомарно переименовываем:
def upgrade():
op.create_table('orders_shadow',
sa.Column('id', sa.Integer, primary_key=False),
postgresql_partition_by='RANGE (created_at)'
)
connection = op.get_bind()
total = connection.execute("SELECT count(*) FROM orders").scalar()
for offset in range(0, total, 1000):
connection.execute(f"""
INSERT INTO orders_shadow
SELECT * FROM orders
ORDER BY id LIMIT 1000 OFFSET {offset}
""")
time.sleep(0.1)
connection.execute("""
ALTER TABLE orders RENAME TO orders_old;
ALTER TABLE orders_shadow RENAME TO orders;
""")
Предостережения, которые обычно игнорируют
* Тестируйте на клоне прода. Не на тестовой базе с 10 строками.
* Используйте
--sql флаг Alembic, чтобы посмотреть, что он сгенерирует, и руками проверить план.* Мониторьте
log_statement и deadlocks. Партиционирование не спасает от блокировок при массовом INSERT.* Имейте rollback-скрипт для каждой фазы. Если что-то пошло на этапе переименования, откат должен быть за секунды.
Вывод: Партиционирование через transactional DDL — это не магия, а расчёт, который окупается, когда таблицы растут на 10% в месяц, иначе проще оставить как есть.
Ленивые импорты в Python 3.15: PEP 810 и буст скорости запуска
На PyCon US 2026 высшие чины из Python-мира вкинули инфу про фичи грядущего Python 3.15. В списке — официальные ленивые импорты (PEP 810). Суть: модули грузятся не при старте твоей софтины, а только когда к ним реально обращаются.
Чел раскурил механику и замерил прирост производительности в PyCharm.
Почитать
👉 About Python
На PyCon US 2026 высшие чины из Python-мира вкинули инфу про фичи грядущего Python 3.15. В списке — официальные ленивые импорты (PEP 810). Суть: модули грузятся не при старте твоей софтины, а только когда к ним реально обращаются.
Чел раскурил механику и замерил прирост производительности в PyCharm.
Почитать
Please open Telegram to view this post
VIEW IN TELEGRAM
С IT-вакансиями уже давно не цветочки — реальная «лимонная» свалка, про которую все в курсе. На Хабре не просто ноют, а пробежались по фактам: что из этого дерьма вытекает для соискателей и для тех, кто нанимает.
Автор не разводит сопли, а тащит годные тулзы для разработчиков и эйчаров, чтоб выжить в этом долбаном цирке.
👉 Читать на Хабре
👉 About Python
Автор не разводит сопли, а тащит годные тулзы для разработчиков и эйчаров, чтоб выжить в этом долбаном цирке.
👉 Читать на Хабре
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1
Парень раскатал гео-аналитическую платформу с нуля за 2,5 месяца. И да, он юзал AI-разработку. Пальцем в код не тыкал — отдал всё на откуп нейронке, хотя сам сомневался, что её хватит на реальный прод.
«Нутром чую: чем жирнее проект, тем быстрее AI теряет нить и долбится в лимиты», — делится автор. Но хрен там: платформу, на которую в доисторические времена потратили бы годы, вдвоём забацали за 2,5 месяца.
Хочешь разобраться, как это работает и что надо чтобы повторить — курить статью на Хабре: читать далее
👉 About Python
«Нутром чую: чем жирнее проект, тем быстрее AI теряет нить и долбится в лимиты», — делится автор. Но хрен там: платформу, на которую в доисторические времена потратили бы годы, вдвоём забацали за 2,5 месяца.
Хочешь разобраться, как это работает и что надо чтобы повторить — курить статью на Хабре: читать далее
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1
⚡️ GPT-5.6 Sol Ultra завалила 50-летнюю гипотезу о циклах
Легендарная задача из теории графов, которую в 1973-м выкатил Дьердь Секереш, наконец-то дождалась. Суть гипотезы: мол, в любом графе без мостов существует такой букет циклов, что каждое ребро входит ровно в два из них. И всё это без шансов на отмазку.
Пару часов назад сотрудник OpenAI дропнул заяву (X): новая GPT-5.6 Sol сварганила доказательство за час, натравив на неё 64 субагента. Файл с решением положили (PDF), но математический бомонд пока молчит — подтверждения нет.
👉 About Python
Легендарная задача из теории графов, которую в 1973-м выкатил Дьердь Секереш, наконец-то дождалась. Суть гипотезы: мол, в любом графе без мостов существует такой букет циклов, что каждое ребро входит ровно в два из них. И всё это без шансов на отмазку.
Пару часов назад сотрудник OpenAI дропнул заяву (X): новая GPT-5.6 Sol сварганила доказательство за час, натравив на неё 64 субагента. Файл с решением положили (PDF), но математический бомонд пока молчит — подтверждения нет.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1
Anthropic толкает самоуправляемых агентов
Команда, стоящая за Claude Code, выкатила гайд по loops — это такие паттерны, где агент гоняет свой рабочий цикл по кругу, пока не долбит стоп-сигнал. Суть проста: завязывай с ручным управлением и постепенно отдавай агенту всё больше контроля.
Всего есть 4 типа циклов:
• Агентный — стартует от промпта, вырубается, когда Claude решает, что работа сделана (ручная верификация через SKILL.md).
• Целевой /goal — ты сам задаёшь условие останова, а отдельная модель-оценщик проверяет результат на соответствие.
• Временной /loop или /schedule — агент сам дёргает триггер: норм для рутины и возни с внешними системами.
• Проактивный — выкидывает человека из цепочки, активируется по событию или по расписанию.
Пара советов в довесок: стартуй с самого простого решения, держи код в чистоте, а на ревью подключай второго агента (/code-review). За токенами следи — ставь чёткие условия останова и выбирай модель с умом. Масштаб динамических воркфлоу проверяй на небольшой выборке, а не сразу на всём.
👉 About Python
Команда, стоящая за Claude Code, выкатила гайд по loops — это такие паттерны, где агент гоняет свой рабочий цикл по кругу, пока не долбит стоп-сигнал. Суть проста: завязывай с ручным управлением и постепенно отдавай агенту всё больше контроля.
Всего есть 4 типа циклов:
• Агентный — стартует от промпта, вырубается, когда Claude решает, что работа сделана (ручная верификация через SKILL.md).
• Целевой /goal — ты сам задаёшь условие останова, а отдельная модель-оценщик проверяет результат на соответствие.
• Временной /loop или /schedule — агент сам дёргает триггер: норм для рутины и возни с внешними системами.
• Проактивный — выкидывает человека из цепочки, активируется по событию или по расписанию.
Пара советов в довесок: стартуй с самого простого решения, держи код в чистоте, а на ревью подключай второго агента (/code-review). За токенами следи — ставь чёткие условия останова и выбирай модель с умом. Масштаб динамических воркфлоу проверяй на небольшой выборке, а не сразу на всём.
Please open Telegram to view this post
VIEW IN TELEGRAM
Память под контролем:
Про
Оверхед словаря и мифы
По умолчанию каждый экземпляр держит атрибуты в
- наследование: у родителя нет
- жесткость: нельзя динамически добавить атрибут - ломаются ORM, сериализаторы, половина библиотек
- дебаг: без
Профилирование через pympler
Вместо гаданий - профилирование через
Цифры не врут: видно, где оверхед реально критичен, а где можно забить.
Гибридная модель:
Когда часть объектов динамическая (dataclass с optional полями), добавляем
Жесткие поля - без оверхеда словаря, динамические - через
Что я обычно делаю:
- профилирую не на старте, а когда память реально начинает течь (pympler.muppy, pympler.summary)
- для тысяч объектов - ORM-модели, объекты игр - обязательно
- если dataclass с наследованием -
- никогда не пихаю
Вывод:
__slots__, pympler и гибридные моделиПро
__slots__ пишут часто, но в реальных проектах с сотнями классов можно нарваться. Не всё так радужно.Оверхед словаря и мифы
По умолчанию каждый экземпляр держит атрибуты в
__dict__. Словарь с хеш-таблицей для миллионов объектов - это 50-70% оверхеда. __slots__ фиксирует список атрибутов на уровне класса и убирает __dict__ (если не добавить вручную). Экономия до 40-60%. Но в больших кодовых базах всплывают грабли:- наследование: у родителя нет
__slots__, у наследника есть - __dict__ все равно создается, выигрыша нет- жесткость: нельзя динамически добавить атрибут - ломаются ORM, сериализаторы, половина библиотек
- дебаг: без
__dict__ не увидишь текущие поля экземпляра в отладчике, приходится лезть в классПрофилирование через pympler
Вместо гаданий - профилирование через
pympler. Пример:from pympler import asizeof
class WithSlots:
__slots__ = ('x', 'y')
def __init__(self):
self.x = 10
self.y = 20
class WithoutSlots:
def __init__(self):
self.x = 10
self.y = 20
print(asizeof.asizeof(WithSlots())) # ~72 байта
print(asizeof.asizeof(WithoutSlots())) # ~184 байта
Цифры не врут: видно, где оверхед реально критичен, а где можно забить.
Гибридная модель:
__slots__ + __dict__Когда часть объектов динамическая (dataclass с optional полями), добавляем
__dict__ в __slots__:class FlexibleSlots:
__slots__ = ('id', 'name', '__dict__')
def __init__(self, id, name, **kwargs):
self.id = id
self.name = name
for k, v in kwargs.items():
setattr(self, k, v)
obj = FlexibleSlots(1, 'test', extra_field=42)
print(obj.extra_field) # 42
Жесткие поля - без оверхеда словаря, динамические - через
__dict__. Памяти чуть больше, но гибкость остается.Что я обычно делаю:
- профилирую не на старте, а когда память реально начинает течь (pympler.muppy, pympler.summary)
- для тысяч объектов - ORM-модели, объекты игр - обязательно
__slots__- если dataclass с наследованием -
@dataclass(slots=True) (Python 3.10+), но контролирую дерево наследования- никогда не пихаю
__slots__ в классы, которые подмешиваются через MRO или передаются в гибкие фреймворкиВывод:
__slots__ - мощный инструмент для сокращения памяти, но его применение требует профилирования и осознанного проектирования, особенно при наследовании и в гибридных сценариях с __dict__.This media is not supported in your browser
VIEW IN TELEGRAM
💀 — 2026-й: железяки полезут в медицину, космос и вообще везде, где мы тупим.
😂 — А в реальности 2026-го: ...
👉 About Python
😂 — А в реальности 2026-го: ...
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1
django-filter — 26.1
На свет появилась свежая версия django-filter 26.1. Если кратко — это такая штука для Django, с помощью которой ты легко настраиваешь фильтрацию для querysets своих моделей. Без лишней магии.
Забрать можно тут: https://pypi.python.org/pypi/django-filter/
👉 About Python
На свет появилась свежая версия django-filter 26.1. Если кратко — это такая штука для Django, с помощью которой ты легко настраиваешь фильтрацию для querysets своих моделей. Без лишней магии.
Забрать можно тут: https://pypi.python.org/pypi/django-filter/
Please open Telegram to view this post
VIEW IN TELEGRAM
PyPI
django-filter
Django-filter is a reusable Django application for allowing users to filter querysets dynamically.
Кастомный семафор с приоритетами и TTL для асинхронных in-memory кэшей
Стандартный asyncio.Semaphore не учитывает приоритеты задач и не имеет time‑to‑live. В production‑кэшах это приводит к тому, что чтение с высоким приоритетом блокируется фоновой записью, а зависшая задача может навсегда захватить ресурс. Решение — семафор на PriorityQueue с TTL.
Архитектура на heapq и TTL
Внутри используется heapq — вставка и извлечение за O(log n). Каждая задача помещается в очередь с приоритетом, а TTL отсчитывается от момента постановки. Если время истекло — выбрасывается TimeoutError. Освобождается слот — подхватывается самая приоритетная задача. Context manager гарантирует вызов release.
Типичные грабли
Если TTL истёк, будущее (Future) нужно явно вычищать из очереди, иначе оно остаётся мёртвым грузом и накапливает память. Также приоритеты инвертированы: heapq считает минимальное значение наивысшим приоритетом. Поэтому в acquire передаётся отрицательное число, если нужно обратное поведение.
Пример из production
В in‑memory кэше расставлены приоритеты: запись от пользователя — 0, фоновое обновление — 5, чтение — 10. Для чтения установлен короткий TTL — при зависании можно отдать stale данные вместо полной блокировки. Это trade‑off между свежестью и доступностью.
Недостаток и решение
Если очередь большая и приоритеты постоянно высокие, низкоприоритетные задачи могут умирать по TTL повторно. В таких сценариев стоит внедрить dynamic priority aging — со временем повышать приоритет ожидающей задачи. Но для большинства кэшей описанного подхода достаточно.
Вывод: Кастомный семафор с приоритетами и TTL решает конкретные проблемы конкурентного доступа в асинхронных кэшах, но требует аккуратной очистки и осознания trade‑offs при голодании низкого приоритета.
Стандартный asyncio.Semaphore не учитывает приоритеты задач и не имеет time‑to‑live. В production‑кэшах это приводит к тому, что чтение с высоким приоритетом блокируется фоновой записью, а зависшая задача может навсегда захватить ресурс. Решение — семафор на PriorityQueue с TTL.
Архитектура на heapq и TTL
Внутри используется heapq — вставка и извлечение за O(log n). Каждая задача помещается в очередь с приоритетом, а TTL отсчитывается от момента постановки. Если время истекло — выбрасывается TimeoutError. Освобождается слот — подхватывается самая приоритетная задача. Context manager гарантирует вызов release.
Типичные грабли
Если TTL истёк, будущее (Future) нужно явно вычищать из очереди, иначе оно остаётся мёртвым грузом и накапливает память. Также приоритеты инвертированы: heapq считает минимальное значение наивысшим приоритетом. Поэтому в acquire передаётся отрицательное число, если нужно обратное поведение.
Пример из production
В in‑memory кэше расставлены приоритеты: запись от пользователя — 0, фоновое обновление — 5, чтение — 10. Для чтения установлен короткий TTL — при зависании можно отдать stale данные вместо полной блокировки. Это trade‑off между свежестью и доступностью.
@asynccontextmanager
async def acquire(self, priority: int, ttl: float):
future = self._loop.create_future()
heapq.heappush(self._queue, (priority, future))
try:
result = await asyncio.wait_for(future, timeout=ttl)
yield result
except asyncio.TimeoutError:
self._cleanup_expired()
raise
finally: ... # release and pop next
Недостаток и решение
Если очередь большая и приоритеты постоянно высокие, низкоприоритетные задачи могут умирать по TTL повторно. В таких сценариев стоит внедрить dynamic priority aging — со временем повышать приоритет ожидающей задачи. Но для большинства кэшей описанного подхода достаточно.
Вывод: Кастомный семафор с приоритетами и TTL решает конкретные проблемы конкурентного доступа в асинхронных кэшах, но требует аккуратной очистки и осознания trade‑offs при голодании низкого приоритета.
Как прокачать инструменты для кодинг-агента
Чувак из блога mathspp.com снова в деле — гнет свою серию про написание кодинг-агента с нуля. В этот раз речь пойдет о том, как запилить для него реально годные инструменты.
Все подробности — по ссылке: https://mathspp.com/blog/write-a-coding-agent-from-first-principles-better-tools
👉 About Python
Чувак из блога mathspp.com снова в деле — гнет свою серию про написание кодинг-агента с нуля. В этот раз речь пойдет о том, как запилить для него реально годные инструменты.
Все подробности — по ссылке: https://mathspp.com/blog/write-a-coding-agent-from-first-principles-better-tools
Please open Telegram to view this post
VIEW IN TELEGRAM
Mathspp
Write a coding agent from first principles: better tools
Improve the capabilities of your agent by providing it with better tools.
Сборка Python-приложения в единый бинарник: избавляемся от зависимостей от системных библиотек с PyInstaller и staticx
Каждый раз, когда нужно задеплоить Python-приложение не в контейнер, а просто скопировать один файл на сервер, всплывает проблема зависимостей от системных библиотек. PyInstaller собирает бинарник, но он всё равно тащит libc, libssl и другие .so-файлы. На старой Ubuntu или в Docker на scratch это приводит к ошибкам при запуске. Опытные разработчики часто забывают, что статическая сборка решает эту проблему радикально.
Почему одного PyInstaller недостаточно
PyInstaller с флагом --onefile упаковывает Python-интерпретатор и код в один файл, но он динамически линкует системные библиотеки. Проверка через ldd покажет кучу зависимостей. На машине с другой версией glibc или отсутствующей библиотекой (например, libcrypto) приложение упадёт. Это типичная ошибка: разработчик уверен, что бинарник самодостаточен, но на проде получает segfault.
Решение: статическая линковка через staticx
staticx переупаковывает полученный PyInstaller-бинарник, заменяя динамические зависимости на статические. Он встраивает musl libc и делает бинарник полностью независимым от системы. Для этого:
На выходе — бинарник, который запускается на любом Linux с ядром >= 2.6.28. Без Python, без pip, без системных библиотек.
Типичные ошибки и trade-offs
* Размер бинарника: 5-30 МБ вместо 5 МБ. Для большинства серверных задач это приемлемо, но для embedded может быть критично.
* Проблемы с glibc-расширениями: staticx использует musl libc, поэтому старые C-расширения (например, NumPy, некоторые ORM) могут сломаться. Решение — собирать всё в Docker на alpine:latest с musl-dev.
* Динамические ctypes: если в коде используются ctypes для загрузки .so, их нужно вручную добавить через --add-binary, иначе приложение упадёт с тишиной.
* Дебаг: ошибки сложнее диагностировать. Помогает флаг --debug в PyInstaller и strace на скомпилированном бинарнике.
Production-пример: CI/CD деплой в корпоративной сети
Представьте: вы разворачиваете сервис на серверах, где IT-отдел одобряет установку только через внутренний репозиторий, а на копирование одного файла — без ограничений. Вы запускаете в CI:
Копируете service_static на сервер — и он работает сразу, без Python, зависимостей и permission battles. Это особенно ценно для edge-устройств и минималистичных Docker-образов на scratch.
Вывод: staticx превращает PyInstaller-бинарник в truly portable решение, но требует учёта C-расширений и динамических загрузок — без этого сборка будет работать нестабильно на проде.
Каждый раз, когда нужно задеплоить Python-приложение не в контейнер, а просто скопировать один файл на сервер, всплывает проблема зависимостей от системных библиотек. PyInstaller собирает бинарник, но он всё равно тащит libc, libssl и другие .so-файлы. На старой Ubuntu или в Docker на scratch это приводит к ошибкам при запуске. Опытные разработчики часто забывают, что статическая сборка решает эту проблему радикально.
Почему одного PyInstaller недостаточно
PyInstaller с флагом --onefile упаковывает Python-интерпретатор и код в один файл, но он динамически линкует системные библиотеки. Проверка через ldd покажет кучу зависимостей. На машине с другой версией glibc или отсутствующей библиотекой (например, libcrypto) приложение упадёт. Это типичная ошибка: разработчик уверен, что бинарник самодостаточен, но на проде получает segfault.
Решение: статическая линковка через staticx
staticx переупаковывает полученный PyInstaller-бинарник, заменяя динамические зависимости на статические. Он встраивает musl libc и делает бинарник полностью независимым от системы. Для этого:
pyinstaller --onefile --name myapp main.py
pip install staticx
staticx dist/myapp dist/myapp_static
На выходе — бинарник, который запускается на любом Linux с ядром >= 2.6.28. Без Python, без pip, без системных библиотек.
Типичные ошибки и trade-offs
* Размер бинарника: 5-30 МБ вместо 5 МБ. Для большинства серверных задач это приемлемо, но для embedded может быть критично.
* Проблемы с glibc-расширениями: staticx использует musl libc, поэтому старые C-расширения (например, NumPy, некоторые ORM) могут сломаться. Решение — собирать всё в Docker на alpine:latest с musl-dev.
* Динамические ctypes: если в коде используются ctypes для загрузки .so, их нужно вручную добавить через --add-binary, иначе приложение упадёт с тишиной.
* Дебаг: ошибки сложнее диагностировать. Помогает флаг --debug в PyInstaller и strace на скомпилированном бинарнике.
Production-пример: CI/CD деплой в корпоративной сети
Представьте: вы разворачиваете сервис на серверах, где IT-отдел одобряет установку только через внутренний репозиторий, а на копирование одного файла — без ограничений. Вы запускаете в CI:
pyinstaller --onefile --name service main.py
staticx dist/service dist/service_static
Копируете service_static на сервер — и он работает сразу, без Python, зависимостей и permission battles. Это особенно ценно для edge-устройств и минималистичных Docker-образов на scratch.
Вывод: staticx превращает PyInstaller-бинарник в truly portable решение, но требует учёта C-расширений и динамических загрузок — без этого сборка будет работать нестабильно на проде.