РозробкаБудь-який рівень

Python

Python — це мова, до якої тягнуться, коли хочуть читати робочий код уже за годину, і та, що зрештою повністю забрала собі роботу з даними й машинне навчання. Це також середовище з найбільш хибно зрозумілою моделлю продуктивності: додавання потоків до програми на Python зазвичай не пришвидшує її взагалі — і з цілком передбачуваної причини, щойно ви її знаєте. На цій сторінці — GIL, типізація, пакування, Django проти FastAPI і те, де Python-проєкти розсипаються.

Байт-код
Один потік водночас
Типи
Підказки, перевіряють інструменти
Релізи
Щороку, 5 років підтримки

Чому більше потоків не допомогло

CPython захищає свої нутрощі одним замком — глобальним блокуванням інтерпретатора, — і потік має тримати його, щоб виконувати байт-код. Потоки справжні, потоки операційної системи, плануються вони звичайно, і лише один із них будь-коли виконує код Python. Запустіть чотири потоки рахувати числа на чотириядерній машині — і отримаєте пропускну здатність одного ядра плюс накладні витрати на перемикання між ними.

Байт-код Python одночасно виконує лише один потік, тож три потоки з навантаженням на CPU по черзі працюють на одному ядрі, скільки б ядер не мала машина, — у лівій панелі в кожному стовпці заповнено один проміжок. Три процеси мають власний інтерпретатор і власне блокування, тож їхні проміжки заповнені всі. Виняток і є причиною тримати потоки: потік, що чекає на сокет, файл чи базу даних, віддає блокування на час очікування, тож робота з вводом-виводом справді накладається.

Друга половина правила й лишає потоки корисними. Потік, що чекає — на сокет, файл, відповідь бази даних, — на час очікування звільняє блокування; так само роблять і 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, де відсутність компілятора відчувається щорелізу.
  • Середовища, куди не можна доставити інтерпретатор із залежностями: єдиного бінарника тут не виходить.

Було корисно?

Поділіться з тим, хто працює над тією ж задачею.