Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 5
×

Що нового в Python 3.11 — функціонал та найголовніші зміни

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Привіт! Мене звуть Олексій. Я студент 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, ми також отримали покращення продуктивності виконання коду на 10-60% або, як показує тестування, нова версія показує в середньому в 1.25 рази більшу швидкість. Нагадаю, що у версії Python 3.11 Гвідо ван Россум (автор мови Python) обіцяв збільшення швидкості у 2 рази. І попри те, що таких вражаючих результатів досягти не вдалось, команда розробників CPython і далі продовжують працювати над реалізацією інтерпретатора, щоб зберегти його позиції.

З усім тим, Python продовжує займати провідні позиції у TIOBE Index у січні 2023 року, обганяючи такі мови програмування як Java, JS, and C/C++.

Підсумки


У цьому оновлені, у порівнянні з минулими, ми отримали:

  1. Невелике збільшення швидкості.
  2. Покращення трасування, що дозволить значно скоротити час на знаходження помилок.
  3. Додавання StrEnum, яке так очікувалося після його брата IntEnum.
  4. Додавання групи винятків та нового оператора except*.
  5. Новий метод add_note() для розширення повідомлення винятків.
  6. Required та NotRequired поля у TypedDict тощо.

Сучасна динаміка ринку потребує постійної актуалізації знань і швидкого реагування на нові тренди. Я впевнений, що найближчим часом нова функціональність оновить список best practices, що означатиме, що ви, як спеціаліст, зможете пришвидшити розробку та підтримку вашого коду.

Дякую за прочитання. Слава Україні 🇺🇦

Джерела:

👍ПодобаєтьсяСподобалось27
До обраногоВ обраному9
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

тільки не tomlib а tomllib

Виправив. Дякую за коментар)

дякую, цікава стаття, хоча я і не пітоніст.
чи є у розробників плани нарешті зробити нормальні лямбди і кастомні інфіксні оператори?

інфіксні оператори

Що це таке в перекладі на англійську?

А може хай це залишиться у LISP подібних мовах?
Нащо у python тягнути усе на світі? Хоча тенденція серед поширених мультипарадигмених мов така спостерігається. Фу, немає сечі терпіти це борошно

префіксна: + 2 3
інфіксна: 2 + 3
постфіксна 2 3 +
оскільки в пітоні всі оператори інфіксні (окрім унарного плюса і мінуса), то я роблю висновок, що ви прочитали ще замало книжок. тож рушайте! відкривайте для себе цікавий і багатогранний світ компутер сайнс.

Вельми дякую)
Стосовно Ваших запитань:
1) А що саме Вам не вистачає у лямбдах?
2) Не думаю, що планують. Проте їх же можна переоголошувати. Список операторів — docs.python.org/...​ng-operators-to-functions.

1) на скільки я знаю в пітоні вони однострокові. принаймні так було, коли я на нього дивився
2) припустимо модулюю вектор об’єктом. хочу визначити векторне і скалярне множення векторів.

припустимо модулюю вектор об’єктом. хочу визначити векторне і скалярне множення векторів.

Колись так робив на сіплюсплюсі, то заюзав
& (Bitwise And) — для скалярного
* (Multiplication) — для векторного
(або навпаки, але суть та сама)

Підписатись на коментарі