Python
Python — це мова, до якої тягнуться, коли хочуть читати робочий код уже за годину, і та, що зрештою повністю забрала собі роботу з даними й машинне навчання. Це також середовище з найбільш хибно зрозумілою моделлю продуктивності: додавання потоків до програми на Python зазвичай не пришвидшує її взагалі — і з цілком передбачуваної причини, щойно ви її знаєте. На цій сторінці — GIL, типізація, пакування, Django проти FastAPI і те, де Python-проєкти розсипаються.
- Байт-код
- Один потік водночас
- Типи
- Підказки, перевіряють інструменти
- Релізи
- Щороку, 5 років підтримки
Чому більше потоків не допомогло
CPython захищає свої нутрощі одним замком — глобальним блокуванням інтерпретатора, — і потік має тримати його, щоб виконувати байт-код. Потоки справжні, потоки операційної системи, плануються вони звичайно, і лише один із них будь-коли виконує код Python. Запустіть чотири потоки рахувати числа на чотириядерній машині — і отримаєте пропускну здатність одного ядра плюс накладні витрати на перемикання між ними.
Друга половина правила й лишає потоки корисними. Потік, що чекає — на сокет, файл, відповідь бази даних, — на час очікування звільняє блокування; так само роблять і C-розширення, які виконують важку роботу в NumPy, pandas і драйверах баз даних. Тож для вводу-виводу потоки справді накладаються, і код, що проводить час у чеканні на інші системи, чудово масштабується на потоках. Не накладається саме виконання байт-коду Python.
asyncio заслуговує на окреме речення, бо до нього часто тягнуться як до загального пришвидшення, а це не воно. Він дає паралельність в одному потоці: багато операцій у польоті, одна виконується, перемикання відбувається на кожному очікуванні. Це чудово пасує сервісу, який тримає тисячі з’єднань. Має він і гостру грань: один блокуючий виклик усередині асинхронної функції зупиняє весь цикл, тож одна бібліотека без підтримки await здатна перекреслити весь задум. Необережне змішування асинхронного й блокуючого коду — найпоширеніший спосіб зробити asyncio-сервіс повільнішим за багатопотоковий, який він замінив.
Це поволі змінюється. У Python 3.13 з’явилася експериментальна збірка без GIL, а 3.12 додав ізоляцію на рівні інтерпретатора, що дозволяє субінтерпретаторам працювати паралельно. Ані те, ані те не є типовим і не є тим, під що вже варто планувати робочу систему, — але напрямок достатньо очевидний, щоб фраза «Python не вміє у кілька ядер» була вже надто сильною. Для всього, що ви будуєте цього року, ваш процес описує саме модель вище.
Динамічна типізація і як команди її переживають
Python перевіряє типи, коли виконує, а не коли ви зберігаєте. Саме це робить його швидким у написанні — і саме це робить велику Python-базу ризикованою: помилка в назві атрибута стає AttributeError у проді, а не червоним підкресленням. Відповіддю, на якій мова зупинилася, стали підказки типів: анотації, які інтерпретатор під час виконання ігнорує і які перевіряч на кшталт mypy чи Pyright використовує, щоб знайти помилку до релізу.
Підказки необов’язкові й поступові, і саме тому їх реально впровадити: анотуйте межі модуля сьогодні, його нутрощі наступного місяця — і перевіряч ставатиме суворішим разом із вами. Найпершу віддачу вони дають рівно там, де дані входять у систему, — і там же самих підказок замало: підказка каже, що має прийти, а не що прийшло. Pydantic закриває цей розрив, валідуючи й приводячи реальні дані до анотації під час виконання, — тому він і лежить під FastAPI та більшістю сучасного Python-коду для API.
Пакування — друга річ, яку Python-команді треба зробити правильно рано, бо збій тут тихий. Кожен проєкт отримує власне віртуальне середовище; залежності оголошують у pyproject.toml; а точні розв’язані версії лягають у lock-файл, який комітять. Без lock-файлу двоє розробників і машина CI працюють із трьома різними наборами залежностей, і різниця виявляється лише як невідтворюваний баґ. Сучасний інструментарій — uv, Poetry або pip із pip-tools — робить усе це; який саме, важить значно менше, ніж наявність бодай одного.
Django, FastAPI і що кожен припускає
Два фреймворки покривають більшість серверного Python, і побудовані вони на протилежних припущеннях, а не є конкурентами різного рівня якості. Django дає повний застосунок — ORM, міграції, автентифікацію, адмінку, форми, шаблони — і очікує, що ви будуватимете всередині нього. FastAPI дає маршрутизацію, валідацію й документацію і очікує, що решту ви оберете самі. Flask досі існує й лишається розумним мінімальним вибором; більшість нової роботи над API йде у FastAPI, бо валідація й згенерована схема OpenAPI беруться з тих самих підказок типів, які ви й так писали.
Django
- Усе в комплекті: адмінка, автентифікація, ORM і міграції є вже першого дня.
- Типово синхронний; асинхронна підтримка є, але ORM дійшов туди лише частково.
- Найкращий для продуктів із моделлю даних, інтерфейсом редактора й справжніми акаунтами.
- Ризик — «товста модель», що росте, доки бізнес-правила можна перевірити лише через в’юхи.
FastAPI
- Асинхронний за задумом, на ASGI — природно пасує сервісам, які здебільшого чекають на інші сервіси.
- Валідація й документація OpenAPI генеруються з ваших підказок типів.
- Найкращий для API, внутрішніх сервісів і всього, що стоїть перед моделлю чи іншою системою.
- Ризик — збирати власний стек: ORM, міграції, автентифікація, адмінка — по одному рішенню за раз.
Одне практичне попередження стосується обох. Python-вебзастосунок не запускає себе в проді сам: він працює під серверним процесом, Gunicorn чи Uvicorn, і кількість воркерів цього сервера — ваша справжня межа паралельності. Через GIL корисна конфігурація — це кілька процесів-воркерів, а не багато потоків в одному, і їхню кількість обмежує пам’ять, а не ядра. Помилка тут — найчастіша причина, чому Python-сервіс, який добре виглядає в бенчмарку, лягає під справжнім трафіком.
Як це виявляється в реальній поставці
Домінування Python у даних і машинному навчанні насправді не про мову — воно про бібліотеки й про те, що їхні гарячі шляхи взагалі не на Python. NumPy, pandas, PyTorch та інші — це тонкі Python-інтерфейси над скомпільованими C, C++ і CUDA. Це варто знати, бо саме звідси випливає, як зробити числовий Python швидким: висловіть операцію як операцію над масивом, яку бібліотека виконає одним викликом, а не як цикл, що крокує елементами в Python. Те саме обчислення, написане двома способами, може відрізнятися на два порядки, — і з тієї самої причини GIL у цьому світі не є такою перешкодою, як звучить.
Дві звички тримають Python-сервіс придатним до підтримки на великому розмірі, і обидві про те, щоб перевіряла машина. Запускайте форматувальник і лінтер — Ruff здебільшого замінив старіший стек і достатньо швидкий, щоб працювати на збереження, — щоб ніхто не витрачав час рев’ю на стиль. І блокуйте злиття на mypy чи Pyright на тій суворості, яку кодова база проходить зараз, підвищуючи її свідомо. Без цього підказки типів вироджуються в декорацію: наявні, хибні й такі, яким довіряють.
Де це вироджується
- Потоки, додані для пришвидшення роботи з CPU, — блокування інтерпретатора гарантує, що не допоможе.
- Блокуючий виклик усередині асинхронного обробника зупиняє цикл подій і для всіх інших запитів.
- Змінювані аргументи за замовчуванням і спільні контейнери рівня класу течуть станом між викликами.
- Залежності без lock-файлу: збірка, що пройшла CI, — не та збірка, що дісталася сервера.
- Підказки типів, написані й ніколи не перевірені, гірші за їх відсутність, бо їм вірять.
- Поелементні цикли по масивах там, де векторизований виклик зробив би те саме в скомпільованому коді.
- Зв’язки Django ORM, зачеплені всередині циклу шаблону, — це запит N+1 у його Python-діалекті.
Коли це застосовувати
Застосовуйте, коли
- Робота з даними, аналітика й машинне навчання, де в екосистеми немає серйозного конкурента.
- API та внутрішні сервіси, чий час іде на очікування, — FastAPI з async пасує цій формі точно.
- Продукти, яким швидко потрібні адмінка, акаунти й модель даних, — саме цим і є Django.
- Автоматизація, скриптинг і склеювання систем, де читабельність важить більше за пропускну здатність.
Уникайте, коли
- Сервіси з навантаженням на CPU, що мають зайняти всі ядра в одному процесі, — саме цього середовище не зробить.
- Критичні за затримкою шляхи в мікросекундах, де накладні витрати інтерпретатора є головною ціною.
- Великі кодові бази без перевірки типів у CI, де відсутність компілятора відчувається щорелізу.
- Середовища, куди не можна доставити інтерпретатор із залежностями: єдиного бінарника тут не виходить.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.