Що нового в Python 3.11 — функціонал та найголовніші зміни
Привіт! Мене звуть Олексій. Я студент 4 курсу Київського політехнічного інституту і вже протягом чотирьох років програмую на Python. Сьогодні я хочу продовжити рубрику про #найголовніші_фічі, які зʼявляються з новими версіями Python, і чому їх варто використовувати у своїх проєктах 🙂.
Якщо ви ще не читали що нового в Python 3.10, то раджу це виправити, перш ніж переходити до цієї статті, адже там я розповідаю про перехід з Python 3.6 до Python 3.10, і чому перехід на нові версії — це не тільки важливо, а ще й корисно 😉.
Зміни у Python 3.11
Release Date: October 2022
1. Точні місця розташування помилок у трасуванні
Процес виявлення помилок став набагато простішим, адже під час трасування інтерпретатор тепер вказує не тільки на рядок коду, а й на місце, де виникає помилка. Маленька зміна, яка зекономить багато вашого часу. Ці розширені помилки також можуть бути корисними при роботі з глибоко вкладеними об’єктами dict і кількома викликами функцій.
Приклад Python 3.10:
a = 5 b = 0 result = a / b >>> Traceback (most recent call last): ... result = a / b ... ZeroDivisionError: division by zero
Приклад Python 3.11:
a = 5 b = 0 result = a / b >>> Traceback (most recent call last): ... result = a / b ... ~~^~~ ... ZeroDivisionError: division by zero
Для кращого розуміння, створімо випадковий словник:
a = dict(b=dict(c=dict(d=dict(f=dict())))) a['b']['c']['d']['f'] >>> {}
Тепер спробуємо звернутись до вигаданого ключа в одному з вкладених словників.
Приклад Python 3.10:
a['b']['c']['d']['e'] >>> KeyError Traceback (most recent call last) ... ----> 1 a['b']['c']['d']['e'] ... KeyError: 'e'
Приклад Python 3.11:
a['b']['c']['d']['e'] >>>> Traceback (most recent call last): ... a['b']['c']['d']['e'] ... ~~~~~~~~~~~~~~~~^^^^^ ... KeyError: 'e'
2. Групи винятків, except*
Новий стандарт представляє функції мови, які дозволяють програмі викликати й обробляти декілька винятків одночасно. Вбудовані типи ExceptionGroup і BaseExceptionGroup дають змогу згрупувати винятки та «викинути» їх разом, а новий синтаксис except* узагальнює винятки для відповідності підгрупам груп винятків.
Ось приклад з PEP-654:
import traceback eg = ExceptionGroup( ... "ExceptionGroup#1", ... [ ... TypeError(1), ... ExceptionGroup( ... "ExceptionGroup#2", ... [ValueError(2)] ... ), ... ExceptionGroup( ... "ExceptionGroup#3", ... [OSError(3)] ... ) ... ] ... ) traceback.print_exception(eg) >>> | ExceptionGroup: ExceptionGroup#1 (3 sub-exceptions) ... +-+---------------- 1 ---------------- ... | TypeError: 1 ... +---------------- 2 ---------------- ... | ExceptionGroup: ExceptionGroup#2 (2 sub-exceptions) ... +-+---------------- 1 ---------------- ... | TypeError: 2 ... +---------------- 2 ---------------- ... | ValueError: 3 ... +------------------------------------ ... +---------------- 3 ---------------- ... | ExceptionGroup: ExceptionGroup#3 (1 sub-exception) ... +-+---------------- 1 ---------------- ... | OSError: 4 ... +------------------------------------
Але до чого ж тут except*? Як пишуть у PEP, зірочка — це індикатор того, що кожна група винятків може бути відловлена в except*.
Наведу приклад використання except* з тією ж групою винятків:
import traceback eg >>> ExceptionGroup('ExceptionGroup#1', [ ... TypeError(1), ... ExceptionGroup('ExceptionGroup#2', [ ... TypeError(2), ValueError(3)] ... ), ... ExceptionGroup('ExceptionGroup#3', [OSError(4)])] ... ) try: raise eg except* TypeError as e: traceback.print_exception(e) except* Exception as e: traceback.print_exception(e) >>> + Exception Group Traceback (most recent call last): ... | File "<stdin>", line 2, in <module> ... | File "<stdin>", line 2, in <module> ... | File "<stdin>", line 2, in <module> ... | [Previous line repeated 2 more times] ... | ExceptionGroup: ExceptionGroup#1 (2 sub-exceptions) ... +-+---------------- 1 ---------------- ... | TypeError: 1 ... +---------------- 2 ---------------- ... | ExceptionGroup: ExceptionGroup#2 (1 sub-exception) ... +-+---------------- 1 ---------------- ... | TypeError: 2 ... +------------------------------------ ... ... + Exception Group Traceback (most recent call last): ... | File "<stdin>", line 2, in <module> ... | File "<stdin>", line 2, in <module> ... | File "<stdin>", line 2, in <module> ... | [Previous line repeated 2 more times] ... | ExceptionGroup: ExceptionGroup#1 (2 sub-exceptions) ... +-+---------------- 1 ---------------- ... | ExceptionGroup: ExceptionGroup#2 (1 sub-exception) ... +-+---------------- 1 ---------------- ... | ValueError: 3 ... +------------------------------------ ... +---------------- 2 ---------------- ... | ExceptionGroup: ExceptionGroup#3 (1 sub-exception) ... +-+---------------- 1 ---------------- ... | OSError: 4 ... +------------------------------------
Але хочу зазначити, що групи винятків — це досить специфічне доповнення до базових класів винятків, яке існувало у власних інтерпретаціях багатьох бібліотек і фреймворках. Їхнє використання може бути зумовлено лише потребою одночасного отримання винятків, що рідко трапляється у житті звичайного програміста.
Окрім цього, групи винятків роблять код складнішим, а ситуацій, коли їхнє використання виправдане, зовсім мало. Тому я би радив обходитись, за можливістю, без них.
3. Новий метод add_note у BaseException
Наступним пунктом буде ще одна зміна у винятках. Був доданий метод add_note() до базового класу BaseException для випадків, коли виключення потрібно розширити записом, і це неможливо зробити під час створення виключення. Список записів, доданих до винятку, можна подивитись у __notes__.
Наприклад:
try: raise ValueError('Amount variable should be Decimal type.') except Exception as e: e.add_note('Addition note.') raise >>> Traceback (most recent call last): >>> File "<stdin>", line 2, in <module> >>> ValueError: Amount variable should be Decimal type. >>> Addition note.
4. Required та NotRequired поля у TypedDict
Додали позначення необовʼязкових полів у TypedDict, що раніше було можливо лише завдяки наслідуванню. Параметр total=True типово свідчить про обовʼязковість полів. Для того, щоб позначити поле як необовʼязкове, нам потрібно було створити другий TypedDict, у якому ми мали наслідувати перший з обовʼязковими полями і оголошувати нові необовʼязкові поля:
from typing import TypedDict class _PersonBase(TypedDict): name: str class Person(_PersonBase, total=False): surname: str
Але тепер ми отримали два нових поля — Required та NotRequired (не плутати з Optional, бо Optional — Union[type, None], що не є відсутністю поля, а початковим значенням).
Наприклад:
from typing import TypedDict, NotRequired class Person(TypedDict): name: str surname: NotRequired[str] # або class Person(TypedDict, total=False): name: Required[str] surname: str m1: Person = {"name": "Oleksii", "surname": "Hlavatskyi"} # OK m2: Person = {"name": "Oleksii"} # OK (surname не є required) m3: Person = {"surname": "Hlavatskyi"} # ERROR (пропущено обовʼязкове поле name)
5. LiteralString
До змін анотації також можна віднести і новий тип — LiteralString. Його слід використовувати, щоб вказати, що параметр функції може бути будь-якого літерального рядкового типу та використовується функціями командної оболонки чи для виконання SQL запитів до бази. Це дозволяє функції приймати довільні літеральні типи рядків, а також рядки, що були створені з інших літеральних рядків. Засоби перевірки типів, як mypy, забракує значення, які не є статичними аргументами, забезпечуючи захист від атак ін’єкцій.
Приклад вразливої анотації функції:
def select_user_(user_name: str): db.execute(f‘select * from {user_name}’) # вразливе місце
Приклад умовно безпечної анотації функції:
from typing import LiteralString def select_user_(user_name: LiteralString): db.execute(f‘select * from {user_name}’)
Використання статичних аналізаторів допоможе проконтролювати поведінку й не допустити можливих вразливостей у вашому коді.
6. StrEnum, IntEnum, IntFlag
Змін також зазнав модуль enum. Поруч з Enum, IntEnum, IntFlag та Flag зʼявляється новий базовий клас — StrEnum. Багато хто чекав на цю фічу, адже часто існувала потреба мати str значення enum`ів. Це, зазвичай, реалізовували наступним чином:
from enum import Enum class FruitEnum(str, Enum): APPLE = 'APPLE' BANANA = 'BANANA' FruitEnum.BANANA == 'BANANA' >>> True
За аналогією, ось як виглядає реалізація вбудованого IntEnum:
class IntEnum(int, Enum): """Enum where members are also (and must be) ints"""
Нагадаю, чому обійтись звичайним enum.Enum було недостатньо:
from enum import Enum class FruitEnum(Enum): APPLE = 'APPLE' BANANA = 'BANANA' FruitEnum.BANANA == 'BANANA' >>> False
Єдиний варіант — використати .value під час порівнянь:
>>> FruitEnum.APPLE == 'APPLE' False >>> FruitEnum.APPLE.value == 'APPLE' True
Раніше порівняння елементів enum’ів з іншими str давав False. Тому часто використовували самописні StrEnum(str, Enum). Але тепер, ми нарешті отримали цей вбудований базовий клас, що є аналогом запису (str, Enum).
from enum import StrEnum class FruitEnum(StrEnum): APPLE = 'APPLE' BANANA = 'BANANA' FruitEnum.BANANA == 'BANANA' >>> True
StrEnum наслідує ReprEnum, що дозволяє отримати однаковий результат між str(FruitEnum.APPLE) та format(FruitEnum.APPLE). Нагадаю, що раніше str(FruitEnum.APPLE) виводив би ‘FruitEnum.APPLE’. Таке ж наслідування окрім StrEnum отримали і IntEnum з IntFlag.
7. Self анотація
Нова анотація Self допоможе легко анотувати методи, що повертають обʼєкт класу.
Приклад з документації:
from typing import Self class MyLock: def __enter__(self) -> Self: self.lock() return self class MyInt: @classmethod def fromhex(cls, s: str) -> Self: return cls(int(s, 16))
8. Новий модуль — tomllib
Цей модуль дає змогу підтримувати парсинг TOML-файлів ( JSON). Наразі, після PEP 518, багато рішень почали реалізовувати системні вимоги для Python проєктів у pyproject.toml на відміну від setup.py. Додавання цього модулю не дасть підтримки написання файлів такого формату, але дозволить вам підвантажувати уже існуючі методом load для файлових обʼєктів та loads для рядків відповідно.
Приклад для підвантаження з TOML файлу:
import tomllib with open("pyproject.toml", "rb") as f: data = tomllib.load(f)
Приклад для підвантаження з TOML рядка:
import tomllib toml_str = """ python-version = "3.11.0" python-implementation = "CPython" """ data = tomllib.loads(toml_str)
9. Асинхронна група задач
Була додана асинхронна група задач TaskGroup — асинхронний контекстний менеджер задач, що дає змогу почекати на виконання кожної задачі перед виходом (exit) з with-блоку. Прикладом може слугувати будь-яка асинхронна група задач, як то відправлення http запитів.
Розглянемо приклад з затримкою у часі:
import asyncio async def wait(delay: int): await asyncio.sleep(delay) print(delay) async def main(): wait_list = [0.5, 2, 1] async with asyncio.TaskGroup() as task_group: for wait_delay in wait_list: task_group.create_task(wait(wait_delay)) asyncio.run(main())
10. Швидкодія
У порівнянні з Python 3.10, ми також отримали покращення продуктивності виконання коду на
З усім тим, Python продовжує займати провідні позиції у TIOBE Index у січні 2023 року, обганяючи такі мови програмування як Java, JS, and C/C++.
Підсумки
У цьому оновлені, у порівнянні з минулими, ми отримали:
- Невелике збільшення швидкості.
- Покращення трасування, що дозволить значно скоротити час на знаходження помилок.
- Додавання StrEnum, яке так очікувалося після його брата IntEnum.
- Додавання групи винятків та нового оператора except*.
- Новий метод add_note() для розширення повідомлення винятків.
- Required та NotRequired поля у TypedDict тощо.
Сучасна динаміка ринку потребує постійної актуалізації знань і швидкого реагування на нові тренди. Я впевнений, що найближчим часом нова функціональність оновить список best practices, що означатиме, що ви, як спеціаліст, зможете пришвидшити розробку та підтримку вашого коду.
Дякую за прочитання. Слава Україні 🇺🇦
Джерела:
10 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів