Краще пізно, ніж ніколи: нові фічі Python 3.9

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

Мене звати Володимир Данилевський, я працюю на позиції Python Developer в компанії Django Stars. Моя стаття пояснює нові фічі Python 3.9, реліз якого був 5 жовтня 2020 року.

Останній реліз Python 3.9 містить фічі для зворотної сумісності, як, наприклад, collections.abc.Mapping, що буде видалений у Python 3.10. Використовуйте тестовий режим щоб побачити всі deprecated errors у вашому коді.

IANA база часових поясів PEP 615

Наразі додалась ще одна «batteries» — zoneinfo.ZoneInfo

Спойлер: для Windows користувачам потрібно завантажити tzdata: python -m pip install tzdata

Цей модуль допоможе для tz-aware timestamps. Так, наприклад:

From datetime import datetime
From zoneinfo import ZoneInfo

timestamp = datatime.now(tz=ZoneInfo("Europe/Kiev"))

Він буде мати читабельну мітку часового поясу, також таким же чином можливо змінити таймзону: timestamp = timestamp.astimezone(ZoneInfo("Europe/London"))

Виконавши команду zoneinfo.available_timezones() можна отримати всі можливі лейби для ZoneInfo, які можна використати для timestamps.

Також цей функціонал можна імпортувати у минулі версії Python через модуль backports який є до версії 3.6 backward compatibility.

try:
    Import zoneinfo
except ImportError:
    from backports import zoneinfo

Новий синтаксис оновлення словників PEP 584

Одна з основних дата-структур отримала новий оператор злиття.

Раніше були наступні види злиття словарів:

>>> fifa_hosts = {2006: "Germany", 2010: "South Africa", 2014: "Brazil"}
>>> winter_olympics = {2006: "Italy", 2010: "Canada", 2018: "South Korea"}

>>> {**fifa_hosts, **winter_olympics}
{2006: 'Italy', 2010: 'Canada', 2014: 'Brazil', 2018: 'South Korea'}

>>> merged = fifa_hosts.copy()
>>> for key, value in winter_olympics.items():
...    merged[key] = value
...
>>> merged
{2006: 'Italy', 2010: 'Canada', 2014: 'Brazil', 2018: 'South Korea'}

Тепер з’явились два нових методи Python для словарів: злиття (|) та in-place злиття (|=).

>>> winter_olympics = {2006: "Italy", 2010: "Canada", 2018: "South Korea"}
>>> fifa_hosts | winter_olympics
{2006: 'Italy', 2010: 'Canada', 2014: 'Brazil', 2018: 'South Korea'}

>>> fifa_hosts |= winter_olympics
>>> fifa_hosts
{2006: 'Italy', 2010: 'Canada', 2014: 'Brazil', 2018: 'South Korea'}

Більш гнучке використання декораторів PEP 614

Зазвичай декоратор має бути іменованим callable objecte — функція або class з __call__ методом. PEP 614 дозволяє бути любим callable. Приклад:

import functools

def normal(func):
    return func

def shout(func):
    @functools.wraps(func)
    def shout_decorator(*args, **kwargs):
        return func(*args, **kwargs).upper()

    return shout_decorator

def whisper(func):
    @functools.wraps(func)
    def whisper_decorator(*args, **kwargs):
        return func(*args, **kwargs).lower()

    return whisper_decorator
DECORATORS = {"normal": normal, "shout": shout, "whisper": whisper}

voice = input(f"Choose your voice ({', '.join(DECORATORS)}): ")

@DECORATORS[voice]
def get_story():
    return """
        Alice was beginning to get very tired of sitting by her sister on the
        bank, and of having nothing to do: once or twice she had peeped into
        the book her sister was reading, but it had no pictures or
        conversations in it, "and what is the use of a book," thought Alice
        "without pictures or conversations?"
        """

print(get_story())

Покращення

Нові методи для str — removeprefix, removesuffix PEP 616

На перший погляд, здається, що це ще один велосипед до вже існуючих: .strip .lstrip .rstrip

Але можна побачити, що вони використовують переданий параметр як набір символів для видалення, а не півстроку. Тому наприклад:

'123 321'.strip('12')
'3 3'

Це вже давно є причиною створення подібних баг-репортів

Нові методи .removesuffix</cpde> <code>.removeprefix видаляють одне входження підстроки і повертає нову строку з видаленою підстрокою.

Більш гнучкі type annotations та type hints PEP 593

Анотації для функцій та змінних з’явилися ще в Python 3.0. Це додало можливість анотувати довільною інформацією про змінні та зворотні значення. Зараз подібне використання майже повністю витіснено type hints — за допомогою mypy можливо провести статичний аналіз коду.

Тепер на додачу до звичайних type hints можна додати коментар що буде доступний в __annotations__ та в хелпері get_type_hints

З модуля typing

from typing import Annotated
 
Leg = Annotated[float, "Side leg lenght"]
Hypotenuse = Annotated[float, "Hypotenuse lenght"]
 
def calculate_leg_lenght(existing_leg: Leg, hypotenuse: Hypotenuse) -> Leg:
   return (hypotenuse**2 - existing_leg**2) ** 0.5

Новий більш потужний парсер PEP 617

Найбільш масштабною змінною став реліз нового парсер. Хоча цього не видно, але це важливий крок на шляху нових рішень та розширення існуючого функціоналу. З самого початку Python використовув простий LL(1) parser для парсингу коду в дерево. Парсить це він в один прохід без повернень, з чого витікають обмеження та проблеми. Так наприклад, це дозволить реалізувати tructural pattern matching (PEP 634).

Guido van Rossum зробив детальний розбір переваг та деталей щодо нового парсеру — PEG (parsing expression grammar) parsers.

Зараз головною ціллю core devs є приводження AST нового парсера до вигляду результату зі старого. Якщо у вас виникають проблеми, або ви хочете порівняти результати нового і старого парсеру, можна використати -X oldparser флаг інтерпретатору.

Покращення швидкодії та багато іншого

Покращено присвоювання змінної в comprehension expression. Присвоювання у конструкції for variable in [some_iterable] тепер таке ж швидке як і звичайне variable = some_value.

Покращено операцію floor_division(//) — тепер вона ~ приблизно на 35% швидше.

Python 3.6.6
timeit.timeit('a = 3.5; b = a // 2', number=500000)
0.05193754100002934

Python 3.9.3 
timeit.timeit('a = 3.5; b = a // 2', number=500000)
0.02754197199999453

Під капотом покращили виклики для створення таких built-ins як — range, tuple, set, frozenset, list, dict завдяки використанню нового «vectorcall protocol» PEP 590. На практиці я не можу це підтвердити як і в треді розробників
UPD. Дякую Дмитру, що вказав на мою помилку, нище наведені оновленні результати.

Python 3.8.9
 python3 -m timeit "dict(**{'a': 2, 'b': 4, 'c': 6, 'd': 8})"
1000000 loops, best of 5: 275 nsec per loop
Python 3.9.3
python3 -m timeit "dict(**{'a': 2, 'b': 4, 'c': 6, 'd': 8})"
500000 loops, best of 5: 470 nsec per loop

Також для коротких ASCII строк пришвидшили `decode` метод так, наприклад приблизно на ~15% швидше:

Python 3.8.9
python3 -m timeit 'd={}; repr(d)' 
2000000 loops, best of 5: 196 nsec per loop

Python 3.9.3
python3 -m timeit 'd={}; repr(d)' 
2000000 loops, best of 5: 179 nsec per loop

Але загалом це мало вплинуло на перфоманс, так зі сторінки патч ноута ми можемо прослідкувати за прогресом:

Python version       3.4     3.5     3.6     3.7     3.8    3.9
--------------       ---     ---     ---     ---     ---    ---

Variable and attribute read access:
read_local           7.1     7.1     5.4     5.1     3.9    3.9
read_nonlocal        7.1     8.1     5.8     5.4     4.4    4.5
read_global          15.5    19.0    14.3    13.6    7.6    7.8
read_builtin         21.1    21.6    18.5    19.0    7.5    7.8
read_classvar_f_c    25.6    26.5    20.7    19.5    18.4   17.9
read_classvar_f_i    22.8    23.5    18.8    17.1    16.4   16.9
read_instancevar     32.4    33.1    28.0    26.3    25.4   25.3
read_instancevar_s   27.8    31.3    20.8    20.8    20.2   20.5
read_namedtuple      73.8    57.5    45.0    46.8    18.4   18.7
read_boundmethod     37.6    37.9    29.6    26.9    27.7   41.1

Variable and attribute write access:
write_local          8.7     9.3     5.5     5.3     4.3    4.3
write_nonlocal       10.5    11.1    5.6     5.5     4.7    4.8
write_global         19.7    21.2    18.0    18.0    15.8   16.7
write_classvar       92.9    96.0    104.6   102.1   39.2   39.8
write_instancevar    44.6    45.8    40.0    38.9    35.5   37.4
write_instancevar_s  35.6    36.1    27.3    26.6    25.7   25.8

Data structure read access:
read_list            24.2    24.5    20.8    20.8    19.0   19.5
read_deque           24.7    25.5    20.2    20.6    19.8   20.2
read_dict            24.3    25.7    22.3    23.0    21.0   22.4
read_strdict         22.6    24.3    19.5    21.2    18.9   21.5

Data structure write access:
write_list           27.1    28.5    22.5    21.6    20.0   20.0
write_deque          28.7    30.1    22.7    21.8    23.5   21.7
write_dict           31.4    33.3    29.3    29.2    24.7   25.4
write_strdict        28.4    29.9    27.5    25.2    23.1   24.5

Stack (or queue) operations:
list_append_pop      93.4   112.7    75.4    74.2    50.8   50.6
deque_append_pop     43.5   57.0     49.4    49.2    42.5   44.2
deque_append_popleft 43.7   57.3     49.7    49.7    42.8   46.4

Timing loop:
loop_overhead         0.5     0.6     0.4     0.3     0.3    0.3

Висновки

Загалом, вводить декілька речей для Quality-of-Life, але найголовніше всередині з новим парсером, що дозволить нові фічі вже у версії 3.10.

Перед використанням важливо перевірити чи немає у вас функціоналу, що був видалений та речей що змінились

Так на сьогодні, остання версія — 3.9.4 яка вийшла 4 квітня, вже мала декілька bug-fixes та виправлених вразливостей.

👍НравитсяПонравилось5
В избранноеВ избранном3
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

Если я ничего не путаю, то тест с созданием dict() некорректен ибо там стоит флаг -s (setup) и само создание словаря выполняется один раз дальше выполняется 50000 null-операций.

Там зʼявилась нова мова, на основі синтаксису пітону. Тільки немає gil, та компілюється в машинний код. Але, їй тільки півроку.
github.com/kuroko-lang/kuroko

«Europe/Kiev»

чогось одразу не зайшло)

Теж засмутився з цього факту, коли шукав потрібну зону в

zoneinfo.available_timezones() 

где про баги ? основная проблема покращень — горка новых багов которые вдруг все рушат

Пожожди, еще в 3.10 паттерн матчинг зальют

Ну добавили немного сахара, опять форсинг type hinting’а в языке с динамической типизацией, основные проблемы не решаются.

А какие основные проблемы?

1. раздутая стандартная библиотека с кучей легаси
2. нет вменяемого стандартного пакетного менеджера
3. нет возможности реюзать синхронный код в асинхронном и наоборот (как, например, в Go)
4. невозможно нормально работать с мультитредами из-за GIL

Ну и еще по мелочи:
— Слабая поддержка ФП
— хотелось бы JIT

JIT обіцяють з 3.12.

1. раздутая стандартная библиотека с кучей легаси

Что именно по-вашему легаси?

3. нет возможности реюзать синхронный код в асинхронном и наоборот (как, например, в Go)

Ну почему же нельзя. В асинхронном любые синхронные вызовы просто будет его блокировать, больше вреда не будет (а где это неизбежно, но надо параллелить — есть пулы ниток). Для синхронного есть несколько стандартных методов сделать однократное включение движка asyncio на один вызов.
Конечно, надо понимать последствия и типовые грабли, но всё вполне успешно решается...

4. невозможно нормально работать с мультитредами из-за GIL

Тут согласен. Но проблема обозначена и будет решаться (хотя бы в режиме нескольких параллельных интерпретаторов в одном процессе).

— Слабая поддержка ФП

А какая будет сильная, в полностью-то динамическом языке?

— хотелось бы JIT

PyPy даёт уже неплохой JIT, где не мешает динамическая природа языка.
С этой точки зрения, лучше бы иметь возможности категорически убивать динамичность там, где она не нужна — например, возможность по ходу жизни объекта переопределить любой его метод и даже поменять класс на ходу.
Обязательные объявления переменных (по заказу, как user strict в Perl или implicit none в Fortran).
Вот после этого уже были бы шансы дать JITʼу реальные возможности для работы.

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