Що нового в Python 3.12 — зміни та поліпшення функціонала, які потрібно знати
Привіт, мене звати Данило. Я Back-end developer у компанії SOFTPRO і вже протягом п`яти років програмую на Python.
Нова версія Python 3.12 викликає інтерес та деякі питання в ІТ-спільноти, тому в цій статті поділюсь своїми думками щодо цього. А також покажу, що додали в мову програмування Python з новою версією та поясню, які це дає нам переваги.
Синтаксичні нововведення
PEP-484 ввів у мову змінні типу PEP-612, які базуються на цій концепції, вводячи специфікації параметрів, а PEP-646 додає змінні змінного типу.
Хоча використання змінних типу стало широко поширеним, спосіб, у який вони вказуються в коді, є джерелом плутанини серед багатьох розробників Python. Існує кілька факторів, які сприяють цій плутанині.
- Правила визначення сфери видимості для змінних типу важко зрозуміти. Зазвичай ці змінні розподіляються в межах загальної сфери видимості, але їхнє семантичне значення є чинним лише за виконання в контексті загального класу, функції або псевдоніма типу.
Це призводить до того, що один екземпляр такої змінної під час використання може повторно використовуватись у кількох загальних контекстах і водночас мати різне семантичне значення. - Псевдонімами загального типу часто користуються неправильно, або просто зловживають ними, бо розробники не повністю розуміють, що аргумент типу потрібно надавати тоді, коли використовується псевдонім типу.
- PEP 483 і PEP 484 представили концепцію «дисперсії» для змінної типу, яка використовується в загальному класі. Такі змінні можуть бути інваріантними, коваріантними або контраваріантними. Більшість розробників досі мають проблеми з розумінням цього принципу, як і я, до речі, але зіштовхнутися з нею може будь-хто під час визначення свого першого загального класу.
- Коли із загальним класом або псевдонімом типу використовується більше ніж один параметр типу, правила впорядкування цих параметрів можуть заплутати. Зазвичай він базується на порядку, у якому вони спочатку з’являються в операторі оголошення псевдоніма класу або типу.
Однак це можна замінити у визначенні класу, включивши базовий клас Generic або Protocol. - Для визначення параметрів типу потрібно імпортувати TypeVar і Generic із модуля typing. Змінним типу, визначеним у глобальній сфері видимості, також потрібно надати імена, які починаються з підкреслення, щоб вказати, що змінна є приватною для модуля.
Глобально визначеним змінним типу також часто дають імена, щоб вказати їх дисперсію, що призводить до громіздких імен, таких як_T_contra
і_KT_co
. Поточні механізми розподілу змінних типу також вимагають від розробника надання надлишкового імені в лапках (наприклад,T = TypeVar("T")
).
Саме над виправленням згаданих незручностей та проблем зосереджується оновлення синтаксису.
Відповідно до PEP-695 ми отримали такі зміни в синтаксисі:
- зміну в синтаксисі оголошення загального типу класу;
- зміни в синтаксисі оголошення загального типу функції;
- зміни в оголошенні псевдоніма загального типу.
Порівняння старого та нового синтаксису оголошення загальних класів, функцій та псевдонімів типів:
Назва |
Python 3.11 |
Python 3.12 |
Оголошення загального класу | from typing import Generic, TypeVar _T_co = TypeVar("_T_co", covariant=True, bound=str) class ClassA(Generic[_T_co]): def method1(self) -> _T_co: ... | class ClassA[T: str]: def method1(self) -> T: ... |
Оголошення загальної функції | from typing import TypeVar _T = TypeVar("_T") def func(a: _T, b: _T) -> _T: ... | def func[T](a: T, b: T) -> T: ... |
Оголошення псевдоніма загального типу | from typing import TypeAlias _T = TypeVar("_T") ListOrSet: TypeAlias = list[_T] | set[_T] | type ListOrSet[T] = list[T] | set[T] |
Новий синтаксис дозволяє оголошувати параметри TypeVarTuple
і ParamSpec
, а також параметри TypeVar
з прив’язкою або обмеженнями:
type IntFunc[**P] = Callable[P, int] # ParamSpec type LabeledTuple[*Ts] = tuple[str, *Ts] # TypeVarTuple type HashableSequence[T: Hashable] = Sequence[T] # TypeVar with bound type IntOrStrSequence[T: (int, str)] = Sequence[T] # TypeVar with constraints
Значення псевдонімів, прив’язок та обмежень змінних типу, створених завдяки цьому синтаксису, оцінюються лише на вимогу. Тобто псевдоніми типів можуть посилатися на інші типи, визначені пізніше у файлі.
Параметри типу, оголошені через список таких параметрів, видимі в межах оголошення та будь-яких вкладених сферах, але не в зовнішній сфері. Наприклад, їх можна використовувати в анотаціях типів для методів загального класу або в тілі класу. Однак їх не можна використовувати в сфері видимості модуля після визначення класу.
Щоб підтримувати цю семантику сфери видимості, введено новий тип сфери видимості — сфера анотації. Сфери анотації поводяться здебільшого як сфери видимості функцій, але по-іншому взаємодіють зі сферами видимості класу.
Це оновлення я особисто вважаю найзнаковішим серед усіх. Воно полегшує синтаксис, дає користувачу простий та зрозумій інструментарій для роботи з типами — основою моделі даних у програмуванні.
Розробка полегшується, бо менше відволікають моменти, про які попіклувалися самі розробники мови програмування замість мене. Тільки через ці зміни я б рекомендував робити перехід на Python 3.12, але на нас чекає ще декілька цікавих нововведень, які можуть спонукати вас задуматись про перехід до новішої версії.
Нові граматичні функції
Усі знають про наявність форматування рядків у Python через так звані f-рядки. У новій версії мови програмування їх також було частково модифіковано. Зміни не можна назвати критичними, але вони спрощують написання коду і виправляють деякі неприємні умовності, які були раніше у цьому типі форматування рядків.
Розглянемо окремо кожен з трьох пунктів.
1. Повторне використання лапок.
У Python 3.11 повторне використання тих самих лапок, як і у f-рядку, призводить до виникнення помилки синтаксису, що вимагає від розробника використовувати інші доступні лапки. У новій версії цей кейс було перероблено. Приклад:
>>> songs = ['Take me back to Eden', 'Alkaline', 'Ascensionism'] >>> f"This is the playlist: {", ".join(songs)}" 'This is the playlist: Take me back to Eden, Alkaline, Ascensionism'
Потрібно зауважити, що до цієї зміни не було ніяких обмежень на кількість вкладених рядків. Обмеженням якраз слугувало повторне використання лапок. У Python 3.11 найбільший вкладений рядок приймав наступний вигляд:
>>> f"""{f'''{f'{f"{1+1}"}'}'''}"""
Тепер обмежень на лапки немає і попередній вираз набуває простішої для сприйняття форми:
>>> f"{f"{f"{f"{1+1}"}"}"}"
2. Юнікод символи та спеціальні символи через бекслеш.
У версії python 3.11 не було можливості додавати у f-рядки символи, які містять у собі бекслеш. До таких символів належить дуже популярний символ перенесення рядка. У новій версії ця неприємна умовність була виправлена. Це дозволило використовувати escape-послідовності Unicode.
Приклад використання у новій версії:
>>> print(f"This is the playlist: {"\n".join(songs)}") This is the playlist: Take me back to Eden Alkaline Ascensionism >> print(f"This is the playlist: {"\N{BLACK HEART SUIT}".join(songs)}") This is the playlist: Take me back to Eden♥Alkaline♥Ascensionism
3. Коментарі в багаторядкових виразах.
У попередній версії Python усі вирази f-рядука повинні визначатися в одному рядку коду. Це ускладнює читання та розуміння написаного коду. У новій версії додали підтримку багаторядкових f-рядків та дали можливість вносити коментарі всередину цього виразу.
Наприклад:
>>> f"{", ".join([ ... 'Три тополі - на три сторони', # Три струни в моїм серці натужено ... 'Марширують наші добровольці у кривавий тан' # Визволяти братів-українців з московських кайдан ... ])}" 'Три тополі - на три сторони, Марширують наші добровольці у кривавий тан'
Поліпшення інтерпретатора
У версії 3.12 нам представили субінтерпретатор GIL, тож тепер вони можуть створюватися з унікальним GIL для кожного інтерпретатора.
Це дозволяє використати всі можливості заліза — наприклад, повністю використати переваги кількох ядер ЦП. Наразі ця можливість доступна тільки шляхом використання
Для створення субінтерпретатора використовується функція Py_NewInterpreterFromConfig((
PyThreadState
**tstate_p, const
PyInterpreterConfig
*config)
:
PyInterpreterConfig config = { .check_multi_interp_extensions = 1, .gil = PyInterpreterConfig_OWN_GIL, }; PyThreadState *tstate = NULL; PyStatus status = Py_NewInterpreterFromConfig(&tstate, &config); if (PyStatus_Exception(status)) { return -1; }
Також з’явилися поліпшення у повідомленнях помилки. Було додано можливість інтерпретатора робити припущення, що саме ви написали неправильно і як це виправити, зважаючи на контекст. Наприклад:
>>> sys.version_info Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'sys' is not defined. Did you forget to import 'sys'? >>> class A: def __init__(self): self.blech = 1 def foo(self): somethin = blech >>> A().foo() Traceback (most recent call last): File "<stdin>", line 1 somethin = blech ^^^^^ NameError: name 'blech' is not defined. Did you mean: 'self.blech'? >>> import a.y.z from b.y.z Traceback (most recent call last): File "<stdin>", line 1 import a.y.z from b.y.z ^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Did you mean to use 'from ... import ...' instead? >>> from collections import chainmap Traceback (most recent call last): File "<stdin>", line 1, in <module> ImportError: cannot import name 'chainmap' from 'collections'. Did you mean: 'ChainMap'?
Нововведення в моделі даних
У версії 3.12 на нас чекають невеликі, але важливі, як на мене, новації у моделі даних. У COLLECTIONS
було додано підтримку buffer protocols
. Класи, які реалізують метод __buffer()__
, тепер можна використовувати у якості buffer types
.
Новий collections.abc.Buffer ABC
забезпечує стандартний спосіб представлення об’єктів буфера, наприклад, в анотаціях типів. Нове перелічення inspect.BufferFlags
представляє позначки, які можна використовувати для налаштування створення буфера.
Приклад використання:
>>> def need_buffer(b: Buffer) -> memoryview: return memoryview(b) need_buffer(b"xy") # ok need_buffer("xy") # rejected by static type checkers
Цей «тип даних» можна використовувати у функціях порівняння типу даних та перевірки, чи є клас — субкласом:
>>> from collections.abc import Buffer >>> isinstance(b"xy", Buffer) True >>> issubclass(bytes, Buffer) True >>> issubclass(memoryview, Buffer) True >>> isinstance("xy", Buffer) False >>> issubclass(str, Buffer) False
Що це нам дає? Гарне питання, відповідь на яке буде очевидним не для всіх.
Якщо ви ніколи не писали анотацій типу для коду, який приймає статичні буфери, то вам буде складно зрозуміти, чим це нововведення вам буде корисним.
Тим, хто вже писав подібні анотації, варто знати, що у новій версії з’явилася можливість перевіряти прямо з коду Python, чи підтримує об’єкт протоколи буферів. Відповідно додалася і підтримка протоколів буферів напряму з коду Python.
Вдосконалення стандартних бібліотек, про які треба знати
Відповідно до офіційних змін у бібліотеках та стандартних функціях маємо:
pathlib.Path
тепер дає можливість субкласування;- модуль os отримав покращення роботи на системі Windows;
isinstance()
підвищили швидкодію від двох до двадцяти разів шляхом використанняruntime-checkable protocol
;- asyncio збільшили швидкодію на 75%;
- були додані підтримки CLI для модулів
uuid
таsqlite3
; - пришвидшено генерацію токенів за допомогою модуля tokenize на 64%.
Згадані зміни не є значними, та загалом спрощують розробку і пришвидшують код, що варто відзначити. Особливо помітним для мене, з усіх згаданих, стало пришвидшення роботи модуля asyncio
.
Ще до цієї категорії я додаю зміни у роботі hashlib
. Відповідно до списку змін, який надали розробники, наразі було змінено реалізацію алгоритмів: SHA1, SHA3, SHA2-384, SHA2-512 та MD5 на реалізацію з проєкту HACL. Ці зміни були внесені з міркувань безпеки, що є логічним і правильним кроком у сучасному світі.
Важливі усунення, видалення та обмеження нової версії
Окремо варто описати зміни у роботі віртуального оточення. Тепер під час його створення за допомогою команди venv
не створюється модуль setuptools
.
Це призводить до відсутності чи недоступності модулів distutils
, setuptools
, pkg_resources
, та easy_install
за замовчуванням у стандартному віртуальному оточенні і їх треба встановлювати окремо. Для встановлення згаданих модулів достатньо виконати команду pip install setuptools
в активованому віртуальному середовищі.
Модуль distutils
теж більше недоступний для використання. Тепер для встановлення модулів використовується модуль setuptools
. Для мене це досить логічний крок від розробників, бо distutils
має погану документацію та занадто надлишковий.
Цей модуль містив у собі функціонал для перетворення каталогу вихідного коду в дистрибутив вихідного коду та деякі форми двійкового дистрибутиву. Через місце distutils
у списку стандартних модулів, багато оновлень інших модулів могли були бути внесені тільки з мажорним релізом.
Ще варто згадати, що у Python wstr
з юнікод об’єктів. Це допомогло зменшити розмір об’єкта str
на 8 байтів, які й були виділені під wstr
.
Загалом ці виправлення є достатньо критичними, бо не всі модулі будуть тепер прямо встановлюватись через усунення distutils. Але ніхто не забороняє нам послуговуватися third-party setuptools
, які все ще використовують реалізацію через цей модуль. Більш детально про цей перехід можна почитати у PEP-632.
Підсумки
Ну ось ми й дійшли до висновків. Судити вам, звісно, з особистого досвіду, та, як на мене, оновлення цікаво вивчити, і я рекомендую на нього переходити. Із цікавинок, які на вас чекають, варто виділити:
- спрощено роботи з загальними типами класу, функції та їхніми псевдонімами;
- внесено зміни в синтаксис f-рядків: підтримка символів з бекслешом та багаторядкових f-рядків з коментарями та без;
- додано можливість конфігурувати субінтерпретатор, але наразі винятково у
C-API; - додано нативну підтримку буферів;
- позбавлено модуля distutils;
- усунено зі стандартного списку модулів віртуального оточення модуль setuptools;
- додано новий декоратор
@typing.override
.
Окремо хочу виділити останній пункт, бо йому, на жаль, не знайшлось місця в попередніх категоріях. Цей декоратор вказує перевірникам типу, що метод призначений для перевизначення методу в суперкласі. Це дозволяє засобам перевірки типів виявляти помилки, коли метод призначений для перевизначення чогось у базовому класі, насправді цього не робить.
Сподіваюсь, що мій огляд допоможе вам поліпшити ваш код чи спонукатиме до вивчення нової мови програмування. Дякую за вашу увагу. Слава Україні!!!
Джерела
Читайте також: Рейтинг мов програмування 2024. TypeScript в трійці лідерів, Python зʼявляється у всіх нішах, а Rust — улюблена мова.
19 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів