Утилиты Python и TypeScript: async, ретраи, типизация
Десять сниппетов на двух языках: asyncio.gather с ограничением, декоратор повторов с задержкой, чтение CSV и разворачивание вложенных списков в Python; разбиение массива, группировка, offset-пагинация, отменяемый fetch и схемы Zod в TypeScript.
Разные языки, одни и те же три задачи: делать много дел разом, не делая их все одновременно; повторять то, что упало; и быть уверенным, что данные той формы, которую вы предполагаете.
Неограниченная конкурентность — это отказ в обслуживании, написанный своими руками
asyncio.gather(*tasks) запускает все задачи немедленно. С десятью задачами это нормально; с десятью тысячами URL это десять тысяч соединений, и падает обычно тот сервис, к которому вы обращаетесь, а не ваш. Semaphore, оборачивающий каждую задачу, ограничивает число одновременных, сохраняя тот же интерфейс, — а выбор этого предела является настоящим решением: это та нагрузка, которую вы готовы навязать чужой инфраструктуре.
Повторам нужен потолок и случайная составляющая
Цикл повторов без экспоненциальной задержки долбит проседающий сервис на полной скорости и замедляет его восстановление. Одной задержки тоже мало: если сотня клиентов упала в один момент, все отступят на одинаковый интервал и повторят попытку тоже одновременно, воспроизведя пик. Разносит их джиттер — рандомизация задержки, — и именно его чаще всего забывают.
Вторая половина решения — что именно повторять. 500 или таймаут повторять стоит; 400 или 403 упадут одинаково сколько угодно раз, и повтор лишь отсрочит ту же ошибку.
Разбиение, группировка и компромисс пагинации
Утилиты на TypeScript намеренно небольшие. Разбиение массива на части важно прежде всего потому, что у API есть лимиты пакета, а у баз — лимиты числа параметров: размер диктуется извне, а не выбирается. Группировка по ключу — это Object.groupBy в актуальных рантаймах, и сниппет показывает типизированный ручной вариант для сред, где его ещё нет.
Offset-пагинация приведена с прямо указанной ценой: база всё равно проходит пропускаемые строки, поэтому тысячная страница медленнее первой. Это верный выбор, когда в интерфейсе есть номера страниц, и неверный для бесконечной ленты — там нужны курсоры.
Валидация на границе
Ценность Zod не в том, что он валидирует, а в том, что результат проверки несёт статический тип, выведенный из той же схемы, — рантайм-проверка и тип времени компиляции не могут разойтись. Объявить интерфейс и привести к нему ответ API значит заявить форму, а не проверить её, и заявление становится ложным в момент изменения API. Разбор на границе означает, что кривой ответ упадёт там, где пришёл, а не тремя слоями глубже.
asyncio.gather с ограничением concurrency
Запускает список async-задач параллельно, но не более N одновременно — через asyncio.Semaphore. Предотвращает перегрузку внешних API или БД при большом числе задач.
Как это работает: asyncio.Semaphore(limit) позволяет не более limit задач работать одновременно. Каждая корутина оборачивается в bounded, которая ждёт свободного слота через async with semaphore. asyncio.gather запускает всё параллельно, семафор регулирует поток.
import asyncio
from typing import TypeVar, Coroutine, Any
T = TypeVar('T')
async def gather_limited(coros: list[Coroutine[Any, Any, T]], limit: int = 10) -> list[T]:
"""Запускает корутины параллельно, не более limit одновременно."""
semaphore = asyncio.Semaphore(limit)
async def bounded(coro: Coroutine[Any, Any, T]) -> T:
async with semaphore:
return await coro
return await asyncio.gather(*(bounded(c) for c in coros))
# Example usage — скачать 100 URL, не более 5 параллельно
import aiohttp
async def fetch(session: aiohttp.ClientSession, url: str) -> dict:
async with session.get(url) as resp:
return await resp.json()
async def main():
urls = [f'https://api.example.com/items/{i}' for i in range(100)]
async with aiohttp.ClientSession() as session:
results = await gather_limited(
[fetch(session, url) for url in urls],
limit=5,
)
return results
Проверка существования файла или директории
Современный кросс-платформенный способ проверить существование файла через pathlib вместо старых функций os.path.
Как это работает: pathlib.Path оборачивает путь файловой системы в объект с методами .is_file()/.is_dir(), что читабельнее и менее подвержено ошибкам, чем строковые вызовы os.path.isfile()/os.path.isdir().
from pathlib import Path
def file_exists(path):
return Path(path).is_file()
def dir_exists(path):
return Path(path).is_dir()
# Example usage
if file_exists('config.json'):
print('Config found')
Разворачивание вложенного списка
Рекурсивно превращает произвольно вложенный список в один плоский список.
Как это работает: для каждого элемента — если это список, функция рекурсивно разворачивает его и добавляет результат целиком; иначе элемент добавляется как есть.
def flatten(items):
result = []
for item in items:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
# Example usage
flatten([1, [2, 3, [4, 5]], 6]) # -> [1, 2, 3, 4, 5, 6]
Чтение CSV-файла в список словарей
Стандартный способ разобрать заголовок CSV-файла в ключи, используя встроенный модуль csv в Python — pandas не нужен для простых случаев.
Как это работает: csv.DictReader автоматически использует первую строку как имена полей, поэтому каждая строка возвращается как словарь, доступный по имени столбца, а не по хрупкому числовому индексу.
import csv
def read_csv(path):
with open(path, newline='', encoding='utf-8') as f:
return list(csv.DictReader(f))
# Example usage
rows = read_csv('users.csv')
for row in rows:
print(row['name'], row['email'])
Декоратор retry с backoff (Python)
Декоратор для повтора нестабильных функций с экспоненциальной задержкой и джиттером. Настраиваемые исключения, количество попыток и задержка.
Как это работает: @wraps(func) сохраняет метаданные оригинальной функции. При каждой неудаче задержка удваивается (base_delay * 2 ** attempt), но ограничена max_delay. Джиттер 0–20% разбрасывает повторы многих клиентов во времени.
import time
import random
from functools import wraps
def retry(max_attempts=3, base_delay=0.2, max_delay=2.0, exceptions=(Exception,)):
"""Повторяет вызов с экспоненциальной задержкой и джиттером."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as exc:
if attempt >= max_attempts:
raise
delay = min(max_delay, base_delay * 2 ** (attempt - 1))
jitter = random.uniform(0, delay * 0.2)
time.sleep(delay + jitter)
return wrapper
return decorator
# Example usage
import requests
@retry(max_attempts=4, base_delay=0.5, exceptions=(requests.RequestException,))
def fetch_data(url: str) -> dict:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
data = fetch_data('https://api.example.com/items')
Разбить массив на чанки
Разделяет массив на группы заданного размера — удобно для батчевой обработки, пагинации в памяти или отправки данных частями.
Как это работает: итерируется по массиву с шагом size, вырезая слайс на каждом шаге. Последний чанк может быть меньше size, если элементов не хватает.
function chunk<T>(array: T[], size: number): T[][] {
if (size <= 0) throw new RangeError('chunk size must be > 0')
const chunks: T[][] = []
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size))
}
return chunks
}
// Example usage
chunk([1, 2, 3, 4, 5], 2) // [[1,2],[3,4],[5]]
chunk(['a','b','c','d'], 3) // [['a','b','c'],['d']]
// Батчевая отправка в Telegram (лимит 30 сообщений/сек)
const userBatches = chunk(userIds, 25)
for (const batch of userBatches) {
await Promise.all(batch.map((id) => sendMessage(id, text)))
await sleep(1000)
}
Группировка массива по ключу
Аналог lodash groupBy без зависимостей — группирует массив объектов по значению заданного поля в объект Map/Record. TypeScript-типизированная версия.
Как это работает: используется reduce, чтобы накопить элементы в объект, где ключи — значения группирующего поля, а значения — массивы подходящих элементов. В современных средах (Node 21+) можно использовать нативный Object.groupBy.
function groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
return array.reduce((groups, item) => {
const groupKey = String(item[key])
groups[groupKey] = [...(groups[groupKey] ?? []), item]
return groups
}, {} as Record<string, T[]>)
}
// Нативная альтернатива (Node 21+ / Chrome 117+)
// const grouped = Object.groupBy(array, (item) => item.status)
// Example usage
const orders = [
{ id: 1, status: 'paid' },
{ id: 2, status: 'pending' },
{ id: 3, status: 'paid' },
]
groupBy(orders, 'status')
// { paid: [{id:1,...},{id:3,...}], pending: [{id:2,...}] }
Утилиты пагинации (offset/limit)
Типизированные вспомогательные функции для серверной пагинации — вычисляют offset и limit из номера страницы, возвращают метаданные (total pages, hasNext, hasPrev).
Как это работает: paginate переводит page/perPage в offset/limit для SQL-запроса. paginationMeta принимает total и возвращает все метаданные для ответа API — удобно для единообразного формата пагинации.
interface PaginationInput {
page: number
perPage?: number
}
interface PaginationMeta {
total: number
page: number
perPage: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
function paginate({ page, perPage = 20 }: PaginationInput) {
const safePage = Math.max(1, Math.floor(page))
const limit = Math.max(1, Math.min(perPage, 100))
const offset = (safePage - 1) * limit
return { limit, offset, page: safePage }
}
function paginationMeta(total: number, input: PaginationInput): PaginationMeta {
const { page, limit } = paginate(input)
const perPage = input.perPage ?? 20
const totalPages = Math.ceil(total / perPage) || 1
return { total, page, perPage, totalPages, hasNext: page < totalPages, hasPrev: page > 1 }
}
// Example usage — SQL query
const { limit, offset } = paginate({ page: 3, perPage: 10 })
// SELECT * FROM orders LIMIT 10 OFFSET 20
const meta = paginationMeta(135, { page: 3, perPage: 10 })
// { total: 135, page: 3, perPage: 10, totalPages: 14, hasNext: true, hasPrev: true }
useFetch с AbortController — TypeScript
Vue-композабл для fetch-запросов с автоматической отменой предыдущего запроса при новом вызове. Реактивные состояния data, loading и error из коробки.
Как это работает: каждый новый вызов execute сначала отменяет предыдущий запрос через AbortController.abort(). Это предотвращает race condition — устаревший ответ не перезапишет свежий. AbortError игнорируется, остальные ошибки пишутся в error.
import { ref } from 'vue'
export function useFetch<T>() {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
let controller: AbortController | null = null
async function execute(url: string, options: RequestInit = {}) {
// Отменяем предыдущий запрос
controller?.abort()
controller = new AbortController()
loading.value = true
error.value = null
try {
const res = await fetch(url, { ...options, signal: controller.signal })
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
data.value = await res.json()
} catch (e) {
if ((e as Error).name !== 'AbortError') {
error.value = e as Error
}
} finally {
loading.value = false
}
}
function abort() {
controller?.abort()
}
return { data, loading, error, execute, abort }
}
// Example usage
const { data: users, loading, error, execute } = useFetch<User[]>()
// Переключение страниц — старый запрос отменяется автоматически
watch(currentPage, (page) => {
execute(`/api/users?page=${page}`)
})
Zod схемы валидации для API
Type-safe валидация запросов с Zod — парсинг, трансформация и проверка тела запроса, query-параметров и URL-параметров.
Как это работает: Zod-схемы определяют ожидаемую структуру и ограничения входящих данных. parse выбрасывает ошибку при невалидных данных; safeParse возвращает объект-результат для обработки ошибок без try/catch.
import { z } from 'zod'
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
age: z.number().int().positive().optional(),
role: z.enum(['user', 'admin']).default('user'),
})
// Использование с safeParse для читаемых ошибок
const result = CreateUserSchema.safeParse(body)
if (!result.success) {
const messages = result.error.issues.map(
issue => `${issue.path.join('.')}: ${issue.message}`
)
return { ok: false, errors: messages }
}
// result.data полностью типизирован
createUser(result.data)
// Переиспользуемые фрагменты
const PaginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
})
const UuidParam = z.object({
id: z.string().uuid(),
})
// Пример с Express
app.post('/api/users', (req, res) => {
const parsed = CreateUserSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json(parsed.error.flatten())
res.json(createUser(parsed.data))
})
Другие подборки сниппетов
Контактная информация
Оставьте заявку или напишите напрямую в телеграм. Я отвечу в течение 24 часов.