Оновлення Python 3.13 — чи близький кінець GIL
Привіт! Мене звуть Олексій. Я студент другого курсу магістратури у Київському політехнічному інституті імені Сікорського. Програмую на Python уже шість років і це четверта стаття із серії #найголовніші_фічі, які зʼявляються з новими версіями Python.
З моменту написання моєї статті стосовно оновлення Python 3.12 у лютому 2024, його популярність лише зросла. За даними вересневого рейтингу TIOBE Index for September 2024, приріст склав +6.01% у порівнянні з минулим роком, що ще більше зміцнило позиції цієї мови програмування.
Завдяки чому Python став настільки популярним, чому реліз 3.13 був перенесений на тиждень, та до чого тут GIL — усе це у сьогоднішній статті про ключові оновлення Python 3.13.
Зміни у Python 3.13
Release Date: October 2024
Неочікувана затримка у релізі
Офіційний реліз Python 3.13, спочатку запланований на 1 жовтня, було перенесено на 7 жовтня 2024 року. І варто зазначити, що робота над Python 3.13 почалась ще у травні 2023 року і вже у травні наступного версія 3.13 перестала отримувати нові зміни, окрім як виправлення помилок та покращення документації (детальний графік змін у PEP-719). Як це працює?
Цикл розробки Python має чітко визначені етапи, на кожному з яких змінюються завдання і відповідальності основних розробників. Розробка версій базується на принципі «major.minor.micro» — про семантичне версіонування можна почитати окремо.
Окрім фінальних версій, Python також має попередні, які позначаються як Alpha, Beta та Release Candidate. Вони призначені для тестування досвідченими користувачами, а не для використання у виробничих (production) середовищах.
Етапи розробки
Залежно від етапу, на якому знаходиться поточна версія Python, змінюються і вимоги до внесення змін у вихідний код:
Чітка структура допомагає координувати зусилля команди розробників та гарантує стабільність кожної нової версії Python. То що ж сталося?
Затримка була необхідна у зв’язку з виявленням значного падіння продуктивності, викликаного змінами в інкрементному циклічному збирачі сміття (incremental cyclic garbage collector), впровадженими в попередніх альфа-версіях.
Щоб забезпечити оптимальну продуктивність Python 3.13, було прийнято рішення відкотити зміни в збірнику сміття і застосувати кілька критичних виправлень. Це призвело до випуску додаткового реліз-кандидата (3.13.0rc3), який, як очікується, стане фінальною версією Python 3.13.0, якщо не з’являться нові критичні проблеми.
Включеним повністю або частково у патч-релізах він не буде. Повернуть новий збирач сміття у наступних мінорних версіях (3.14, 3.15 та інші), оскільки додавання нового функціоналу у патч-релізі порушує процеси. Незважаючи на це, версія 3.13 залишається повнофункціональною, і жодних змін в ABI не було зроблено з часів бета-версії.
Інтерактивний інтерпретатор REPL
Покращений інтерактивний інтерпретатор на основі PyPy.
Тепер, коли користувач запускає REPL, цикл читання-обчислення-друку (Read-eval-print loop) з інтерактивного терміналу, його зустрічає підтримка наступних змін:
- Багаторядкове редагування зі збереженням історії.
Уявіть, що ви оголошуєте декоратор у REPL:
І тут згадуєте, що помилились і повертаєте у decorator обʼєкт функції func замість wrapper. Звичайно, до цього (за прикладом вище) прийти не легко. Тому щоб показати, що wrapper не викличеться, спробуймо знову і додамо виведення «decorator» перед виконанням функції, що обгортаємо у wrapper:
Виконуємо знову і бачимо лише «Hello World!» — а очікуваного виведення decorator не сталося. Що робити? Звичайно, якщо скажете декілька разів тиснути вгору і змінити return func на return wrapper ↑, будете праві:
Тепер вже інші справа. І все це завдяки підтримці багаторядкового редагування Й збереження історії. Адже треба памʼятати, що до версії 3.13 ви не могли так легко повернутись до оголошення своєї функції чи класу (декоратора у нашому прикладі) і з легкістю доповнити їх усім необхідним.
Натискання на стрілку вгору не дозволяло редагувати багато рядків, а давало змогу редагувати лише один.
Натиснув один раз:
Натиснув другий раз:
- Пряма підтримка команд REPL, таких як help(), exit() і quit(), без необхідності викликати їх як функції — help, exit, quit.
- Підказки та відстеження з кольором, увімкненим за замовчуванням.
- Інтерактивний перегляд довідки за допомогою F1 з окремою історією команд.
- Перегляд історії за допомогою клавіші F2.
- «Режим вставки» з F3, що полегшує вставлення великих блоків коду.
Щоб вимкнути нову інтерактивну оболонку, установіть змінну середовища PYTHON_BASIC_REPL.
Експериментальна можливість вимкнення глобального блокування інтерпретатора (GIL)
У новій версії Python 3.13 зроблено великий крок до зміни головної родзинки інтерпретатора Python — GIL. Хоча зараз тільки обговорюються шляхи повного позбавлення від GIL, експериментальна можливість вимкнення глобального блокування інтерпретатора уже наявна.
Чи є GIL серйозною проблемою? Згадаймо слова Ларрі Хастінгса на PyCon 2015.
Я б сказав, що дизайнерське рішення GIL є однією з речей, які зробили Python таким популярним, як сьогодні. Тому я не погоджуюся з людьми, які кажуть, що GIL є жахливим і його ніколи не слід було додавати. Ні, я так не думаю. Причина, чому Python став успішним — це тому, що він отримав GIL.
І тепер за 8 років після цих слів ми бачимо, що Python все ж рухається в сторону усунення GIL. Роздуми на тему того, чи спричинить це безпекові проблеми та чи дійсно відсутність GIL так потрібна розробникам, заслуговує на окрему статтю. Тож лише підсвітимо те, що змінилося цитатою з документації і наведемо приклад.
У новій версії CPython з’явилася експериментальна підтримка виконання без глобального блокування інтерпретатора (GIL), що дозволяє працювати в режимі вільного потокового виконання. Ця функція наразі є експериментальною і не активована за замовчуванням.
Щоб скористатися вільнопотоковим режимом, необхідно використовувати інший виконуваний файл, зазвичай під назвою python3.13t або python3.13t.exe. Попередньо зібрані бінарні файли, позначені як «вільнопотокові», можуть бути встановлені через офіційні інсталятори для Windows і macOS. Також можливе складання CPython з вихідних кодів за допомогою опції —disable-gil.
Вільнопотокове виконання дозволяє повністю використовувати потужність процесора, дозволяючи потокам виконуватися паралельно на кількох ядрах CPU. Хоча не всі програми автоматично отримають від цього перевагу, програми, розроблені з урахуванням потоків, працюватимуть швидше на багатоядерних системах. Оскільки функція експериментальна, користувачі можуть стикнутися з певними помилками та суттєвим зниженням продуктивності при одноядерній роботі. У вільнопотокових версіях CPython є можливість увімкнути GIL під час виконання, використовуючи змінну середовища PYTHON_GIL або параметр командного рядка -X gil=1.
Щоб перевірити, чи підтримує поточний інтерпретатор вільнопотоковий режим, можна скористатися командою python -VV або перевірити значення в sys.version, де буде вказано «experimental free-threading build». Також доступна нова функція sys._is_gil_enabled(), яка дозволяє перевірити, чи дійсно GIL відключений у поточному процесі.
А тепер погляньмо на приклади з ThreadPoolExecutor та ProcessPoolExecutor:
import concurrent.futures import contextlib import time from typing import Generator @contextlib.contextmanager def time_it(what: str) -> Generator[None]: t0 = time.monotonic() try: yield finally: print(f'{what} took {time.monotonic() - t0}s') def do_work() -> int: with time_it('work'): x = 0 for _ in range(10_000_000): x += 1 return x def main() -> None: with time_it('main'): with concurrent.futures.ThreadPoolExecutor(4) as pool: futures = [pool.submit(do_work) for _ in range(10)] for future in concurrent.futures.as_completed(futures): print(f'got {future.result()}') if __name__ == '__main__': main()
Та:
def main() -> None: with time_it('main'): with concurrent.futures.ProcessPoolExecutor(4) as pool: futures = [pool.submit(do_work) for _ in range(10)] for future in concurrent.futures.as_completed(futures): print(f'got {future.result()}')
Локальні виконання з ThreadPoolExecutor та ProcessPoolExecutor на версії 3.12 або ж 3.13 з GIL дають такі результати для ThreadPoolExecutor:
work took 1.9548044349066913s got 10000000 work took 2.0012483169557527s work took 2.093829107005149s got 10000000 got 10000000 work took 2.3259086270118132s got 10000000 work took 1.852884995052591s got 10000000 work took 2.2110034850193188s work took 2.244857894955203s got 10000000 got 10000000 work took 2.1200398980872706s got 10000000 work took 1.4704015370225534s got 10000000 work took 1.043533139047213s got 10000000 main took 5.31929242098704s
А також для ProcessPoolExecutor:
work took 0.5303010790375993s work took 0.5309579480672255s got 10000000 got 10000000 work took 0.5479815760627389s got 10000000 work took 0.5544647829374298s got 10000000 work took 0.5440159901045263s got 10000000 work took 0.5461697049904615s got 10000000 work took 0.5407423679716885s got 10000000 work took 0.5399765850743279s got 10000000 work took 0.49855045799631625s got 10000000 work took 0.5028546449029818s got 10000000 main took 1.613782429951243s
Проте у виконанні на версії 3.13 без GIL отримаємо для ThreadPoolExecutor:
work took 0.621877092984505s got 10000000 work took 0.6247945779468864s got 10000000 work took 0.6302563319914043s got 10000000 work took 0.6311170549597591s got 10000000 work took 0.6294306690106168s got 10000000 work took 0.6304955079685897s got 10000000 work took 0.6371516119688749s got 10000000 work took 0.6388808720512316s got 10000000 work took 0.6403557029552758s got 10000000 work took 0.6410054829902947s got 10000000 main took 1.8992961760377511s
Та для ProcessPoolExecutor:
work took 0.6460460430243984s got 10000000 work took 0.6548925309907645s got 10000000 work took 0.688525396049954s got 10000000 work took 0.6899377160007134s got 10000000 work took 0.6749819609103724s work took 0.6663011440541595s got 10000000 got 10000000 work took 0.6706065980251878s got 10000000 work took 0.6761910850182176s got 10000000 work took 0.6525284160161391s got 10000000 work took 0.6593533849809319s got 10000000 main took 2.0157317529665306s
Підібʼємо підсумки загального часу виконання:
Ми спостерігаємо значне пришвидшення у виконанні MultiThreading, проте бачимо, що MultiProcessing отримав зворотній ефект — затримку у 20%. З чим це повʼязано та чи буде це виправлено, поки невідомо. Тож це гарна можливість поексперементувати з режимом вільного потокового виконання (free-threaded) самостійно.
Тим часом рекомендується продовжувати використовувати інтерпретатор з GIL у ваших робочих проєктах, адже це та стабільна версія 3.12, яка не зазнала серйозних змін у швидкодії.
Експериментальна компіляція Just-In-Time (JIT)
Мотивація зробити Python швидше зрозуміла. Що ж таке цей JIT?
Компілятор JIT (Just-In-Time) — це інструмент, який компілює код Python в машинний код «на льоту», під час виконання програми. Він генерує код дуже швидко і легко підтримується. Це дає змогу значно прискорити виконання програм. Одним із поширених рішень JIT-компіляторів є Numba, але зараз ми говоримо про продовження роботи по інтеграції JIT у CPython.
А як працюватиме JIT-компілятор у Python доки не відомо. PEP-744 говорять про те, що він заснований на архітектурі Copy-and-Patch. Компілює байткод Python у машинний код, використовуючи LLVM. Проте цей PEP-744 досі знаходиться у статусі чорнетки — Draft. І зазначається наступне:
Якщо ви програміст на Python або кінцевий користувач, для вас нічого не змінюється. Ніхто не повинен розповсюджувати вам інтерпретатори CPython із підтримкою JIT, поки це все ще експериментальна функція. Коли вона перестане бути експериментальною, ви, ймовірно, помітите трохи кращу продуктивність і трохи більше використання пам’яті. Ви не повинні відчути жодних інших змін.
Покращення повідомлень помилок
Протягом останніх років я помітив тенденцію — кожної осені нова версія Python дарує задоволення від корисних покращень повідомлень помилок.
Погляньмо на приклад, де виклик функції f() спричиняє помилку через NameError (відсутність змінної string).Тепер у виводі повідомлень про помилку відслідкувати послідовність виконань функцій та дерева помилок не є проблемою.
Приклад:
Наступне покращення у повідомленні про помилку — покращення повідомлення при імпорті стандартного модуля, який має ідентичну назву з тим, що виконується.
Так це виглядало у Python 3.12:
А так у Python 3.13:
Оптимізація документаційних-рядків або doc-рядків
Зменшення використання пам’яті шляхом видалення початкових відступів у рядках документів.
Приклад:
>>> class Fruit: ... """ ... Fruit class. ... """ ... >>> Fruit.__doc__ '\nFruit class.\n' >>>
Не варто перейматись за змінні, які ви оголошуєте потрійними лапками. Оптимізація стосується лише документаційних коментарів і не зачіпає ваших багаторядкових оголошень текстових значень:
>>> message = """ ... Hello ... World ... """ >>> message '\n Hello\nWorld\n '
Елементи тільки для читання у TypeDicts
У Python з’явилася можливість оголошувати поля в TypedDict тільки для читання за допомогою нового типу ReadOnly. Це важливе нововведення, яке допомагає покращити типізацію та уникнути непередбачених змін у критичних структурах даних.
Приклад з документації:
class Movie(TypedDict): title: ReadOnly[str] year: int def mutate_movie(m: Movie) -> None: m["year"] = 1999 # Окей m["title"] = "The Matrix" # Помилка типізатора
У цьому випадку поле title позначено як ReadOnly[str], тобто що його значення не можна змінити після створення об’єкта. Якщо хтось все ж спробує це зробити, статичний аналізатор на кшталт mypy повідомить про помилку ще до того, як програма запуститься. Це особливо корисно для API, де важливо, щоб вхідні дані залишалися незмінними, аби уникнути несподіваних збоїв або неправильних змін.
Декоратор deprecated
Було додано deprecated, що значно полегшує процес знаходження та виправлення застарілого коду. Тепер маємо змогу маркувати функції чи класи декоратором для демонстрації того, що вони тепер є застарілими і не рекомендовані для використання. Та пропонувати альтернативи.
Це важливо для підтримки чистого та актуального коду, адже застарілі функції та класи часто залишаються в проектах і можуть викликати помилки або проблеми з сумісністю в майбутніх версіях. Використовуючи @deprecated, розробники зможуть отримувати попередження під час використання таких елементів, що допоможе швидше виявляти та оновлювати старий код.
Приклад:
from warnings import deprecated from typing import overload @deprecated("Use B instead") class A: pass @deprecated("Use g instead") def f(): pass A() f()
Старіння версії Python 3.8
Разом з виходом Python 3.13, підтримка якого обіцяється до жовтня 2029 року, ми отримуємо закінчення безпекової (security) підтримки Python 3.8. Чому так?
Python підтримує кожну версію протягом приблизно п’яти років, а з випуску 3.8 саме стільки уже і минуло. Основна підтримка, тобто оновлення функцій та виправлення помилок, триває близько двох років. Після цього версія переходить у фазу розширеної підтримки, де випускаються лише важливі оновлення безпеки. Головною причиною такого підходу можна виділити те, що підтримка вимагає великої ресурсоємності. Це зупиняє динамічність розробки нових версій.
Два роки активної підтримки дозволяють розробникам Python швидше впроваджувати нові функції та покращення, не затримуючись на підтримці старих версій. Це спонукає розробників та компанії переходити на останні версії, щоб отримувати всі переваги нових функцій та поліпшеної продуктивності.
А ось так виглядає цей релізний цикл:
Отже, з моменту випуску поточної версії додаткові оновлення та виправлення помилок у Python будуть приходити тільки на версії Python 3.12 та Python 3.13. Тож для розробників, які ще не мігрували проєкти, може бути дійсно важливим почати переїздити з уже далеких 3.6, 3.7 а тепер і 3.8, до підтримуваних версій (краще відразу до 3.12 😁).
Щоб мігрувати та розібратись в змінах Python було простіше, читайте попередні статті, де я доступно про це розповів.
А саме:
Що нового в Python 3.10. Функціонал та найголовніші зміни
Що нового в Python 3.11 — функціонал та найголовніші зміни
Оновлення Python 3.12 — чи справді мова тепер вдвічі швидша. Ключові зміни
Висновок
Тож як відбулось оновлення Python 3.13 та що ми в ньому отримали:
- Він вийшов пізніше, ніж очікувалося, через складність змін, пов’язаних з експериментальним вимкненням GIL. Цей новий підхід вимагав часу для тестування та стабілізації. Було покращено досвід використання інтерпретатора, що зробило його більш інтерактивним і зручним для користувачів. Додані нові можливості й покращена підтримка кодування «на льоту».
- Глобальне блокування інтерпретатора (GIL) більше не є обов’язковим завдяки експериментальній функції. Це дозволяє програмам використовувати багатопотоковість ефективніше, що відкриває двері до кращої продуктивності на багатоядерних процесорах. Помилки тепер відображаються з більшим рівнем деталізації та зрозумілості. Це значно полегшує налагодження і робить Python більш дружнім до розробників.
- Оптимізація документування в Python дозволяє використовувати doc-рядки більш ефективно, що впливає як на швидкість виконання коду, так і на зручність роботи з документацією. Нововведення ReadOnly для TypedDict гарантує, що певні поля залишаться незмінними, а це додає безпеки і передбачуваності при роботі зі структурованими даними.
- Тепер Python має вбудований декоратор deprecated, який полегшує процес виявлення застарілого коду, дозволяючи розробникам ефективніше оновлювати свої проєкти.
Зміни, яких зазнав Python 3.13, приємні, але не критичні — бо більшість з них не дають функціональних переваг, а лише створюються поле для експериментів та тестувань.
Попри це, як я зазначив вище — потрібно завжди слідкувати за тенденціями ринку і використовувати нові технологій, що дасть вам конкуренту перевагу в умовах постійної появи нових викликів.
Дякую за прочитання. Слава Україні 🇺🇦
20 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів