Що нового в Python 3.10. Функціонал та найголовніші зміни

Усі статті, обговорення, новини для початківців — в одному місці. Підписуйтеся на телеграм-канал!

Привіт! Мене звуть Олексій. Я — розробник та студент 4-го курсу Київського політехнічного інституту. Уже протягом чотирьох років програмую на Python. Хочу розповісти вам найголовніші фічі, які зʼявились за останні роки, і чому їх варто використовувати у себе в проєкті.

Python вже тривалий час займає ключові позиції у списку найпопулярніших мов програмування, а на час написання статті займає перше місце по рейтингах TIOBE Index for July 2022 & PYPL PopularitY of Programming Language, випереджаючи Java, JS, та C/C++.

Ця стаття буде корисна розробникам, які вже довго не переходять на нові версії Python, бо вважають, що ніякої користі не отримають. Я ж спробую вас переконати у тому, що перехід на нові версії не тільки важливо, а ще і зручно.

Зміни у Python 3.8

Release Date: October 2019

1. Можливість створювати рядки для самодокументованих виразів та налагодження програми

Нагадаю, як з Python 3.6 користувачі отримали чудову можливість форматувати рядки використовуючи не тільки метод .format(), а й префікс f перед рядком, та {} для розміщення даних всередину виразу. Часто ми мали потребу називати те, що виводимо, щоб додати конкретики.

Приклад раніше:

from math import pi

class Employee:
    name = 'Oleksii'
    company = 'Flyaps'

print(f'Employee.name={Employee.name}, Employee.company={Employee.company}')
print(f'round(pi, 2) = {round(pi, 2)}')
print(f'pi = {pi:.5f}')

>>> Employee.name=Oleksii, Employee.company=Flyaps
>>> round(pi, 2) = 3.14
>>> pi = 3.14159

З версії 3.8 маємо можливість створювати такі рядки для self-documenting expressions and debugging. Це робити стало ще простіше, ми просто додаємо символ `=` всередину виразу {} і отримуємо точно такий результат.

Приклад зараз:

from math import pi

class Employee:
    name = 'Oleksii'
    company = 'Flyaps'

print(f'{Employee.name=}, {Employee.company=}')
print(f'{round(pi, 2) = }')
print(f'{pi = :.5f}')

>>> Employee.name='Oleksii', Employee.company='Flyaps'
>>> round(pi, 2) = 3.14
>>> pi = 3.14159

2. Новий walrus оператор `:=`

Ми можемо прямо в умові, або циклі, або ще в чомусь виконувати відразу дві дії: присвоїти значення змінній, а потім порівняти вираз. Наведу приклад простого консольного додатку, який виводить все, що введе користувач, поки рядок буде не порожній.

Приклад раніше:

user_input = input()
print(user_input)
while user_input:
    user_input = input()
    print(user_input)

>>> Hello
Hello
>>> World
World

Тут ми присвоюємо результат введення користувача двічі: перед циклом та в тілі циклу. Тепер у нас зʼявився спосіб відразу зберігати результат обчислення під час перевірки умови:

Приклад зараз:

while user_input := input():
    print(user_input)

>>> Hello
Hello
>>> World
World

Як ви бачите, ми виконуємо ті ж дії: спочатку присвоюємо значення змінній user_input, а потім виконуємо перевірку. Якщо вона спрацювала — заходимо у цикл та виводимо вже оновлений зміст user_input, і так поки користувацьке введення не буде порожнім.

Як щодо використання змінної user_input пізніше в коді, після циклу? Приведу ситуацію, коли будемо створювати дві змінні за допомогою walrus operator:

Приклад зараз:

companies = ['Microsoft', 'Google', 'Apple']
if (n := len(companies)) == 1 and (first_company := companies[0]):
    print(f'We have only one company: {first_company}')
elif (n := len(companies)) > 1 and (first_company := companies[0]):
    print(f'We have such companies as: {", ".join(companies)}')
else:
    print(f'We have no companies')

>>> We have such companies as: Microsoft, Google, Apple

Спробуймо вивести значення поза умовним оператором.

Приклад зараз:

companies = ['Microsoft']
if (n := len(companies)) == 1 and (first_company := companies[0]):
    print(f'We have only one company: {first_company}')
elif (n := len(companies)) > 1 and (first_company := companies[0]):
    print(f'We have such companies as: {", ".join(companies)}')
else:
    print(f'We have no companies')

print(n)
print(first_company)

>>> We have only one company: Microsoft
>>> 1
>>> Microsoft

Проте ви повинні помітити, що Python відкине будь-які перевірки and, якщо перший операнд буде False. Отже, `n` буде створюватись завжди, проте `first_company` — лише тоді, коли вираз `n == 1` або наступний `n > 1` виконаються.

Приклад зараз:

companies = []
if (n := len(companies)) == 1 and (first_company := companies[0]):
    print(f'We have only one company: {first_company}')
elif (n := len(companies)) > 1 and (first_company := companies[0]):
    print(f'We have such companies as: {", ".join(companies)}')
else:
    print(f'We have no companies')

print(n)
print(first_company)

>>> We have no companies
>>> 0
Traceback (most recent call last):
  File "walrus.py", line 10, in <module>
    print(first_company)
NameError: name 'first_company' is not defined

Як можемо помітити, n існує і після тіла умовного оператора if, коли як first_company не був ініціалізований, тому ми отримали помилку NameError: name ’first_company’ is not defined.

Це дає можливість створювати більш лаконічні конструкції, проте головне не загратись. :)

3. Можливість створювати positional only arguments

Приведу пояснювання з офіційної документації:

Тепер користувачі мають можливість обмежувати деяким функціям параметри, які повинні бути передані позиційно та не можуть бути переданими як keyword, positional or keyword, винятково keyword та не можуть бути переданими як positional.

Приклад зараз:

def f(a, b, /, c, d, *, e, g):
    print(a, b, c, d, e, g)

# The following is a valid call:
f(10, 20, 30, d=40, e=50, g=60)

# However, these are invalid calls:
f(10, b=20, c=30, d=40, e=50, g=60)   # b cannot be a keyword argument
f(10, 20, 30, 40, 50, g=60)           # e must be a keyword argument

>>> 10 20 30 40 50 60
Traceback (most recent call last):
  File "parameters.py", line 11, in <module>
    f(10, b=20, c=30, d=40, e=50, g=60)   # b cannot be a keyword argument
TypeError: f() got some positional-only arguments passed as keyword arguments: 'b'

Приведу на прикладі built-in функцій, де ми не повинні передавати keyword, а тільки positional parameters.

Приклад зараз:

def divmod(a, b, /):
    """Emulate the built in divmod() function"""
    return a // b, a % b

divmod(a=1, b=2)

>>> Traceback (most recent call last):
>>>   File "divmod.py", line 5, in <module>
>>>     divmod(a=1, b=2)

>>> TypeError: divmod() got some positional-only arguments passed as keyword arguments: 'a, b'

Тобто нам не можна передавати keywords аргументи `a, b` у функцію divmod. Так само з len(), де ми передаємо тільки positional аргументи:

Приклад зараз:

len(obj='hello')  # The "obj" keyword argument impairs readability

>>> Traceback (most recent call last):
>>>   File "len.py", line 1, in <module>
>>>     len(obj='hello')  # The "obj" keyword argument impairs readability
>>> TypeError: len() takes no keyword arguments

Запитаєте: а як же у такому випадку передавати args/kwargs? Ось як:

Приклад зараз:

def f(a, b, /, *args, **kwargs):
    print(a, b, args, kwargs)

f(10, 20, 1, 2, 3, a=1, b=2, c=3)  # a and b are used in two ways

>>> 10 20 (1, 2, 3) {'a': 1, 'b': 2, 'c': 3}

4. DictReader повертає простий dict замість OrderedDict

Нагадаю, що з Python3.7 ми отримали гарантовано впорядковані dicts, що означає покриття усіх можливих переваг, які раніше були у collections.OrderedDict над dict. Отже, ми отримали логічну зміну повернення результату у модулі csv. Якщо раніше DictReader обовʼязково повертав список з OrderedDict’ів:

Приклад раніше:

import csv

with open('file.csv', 'rt') as f:
    reader = csv.DictReader(f)
    for i in reader:
        print(type(i))
        print(isinstance(i, dict))

>>> <class 'collections.OrderedDict'>
>>> False

То зараз, з версії Python3.7, це вже не має сенсу, тому результатом тепер є не OrderedDict, а простий dict.

Приклад зараз:

import csv

with open('file.csv', 'rt') as f:
    reader = csv.DictReader(f)
    for i in reader:
        print(type(i))
        print(isinstance(i, dict))

>>> <class 'dict'>
>>> True

Зміни у Python 3.9

Release Date: October 2020

1. Можливість використовувати | (ior) з dict

Використання | для dict! Тепер маємо можливість використовувати | (ior) і з dict. Злиття, якщо використовувати |, та оновлення, якщо використовувати як оператор присвоювання |=

Приклад зараз:

x = {'key1': 'value1 from x', 'key2': 'value2 from x'}
y = {'key2': 'value2 from y', 'key3': 'value3 from y'}
print(x | y)
print(y | x)

>>> {'key1': 'value1 from x', 'key2': 'value2 from y', 'key3': 'value3 from y'}
>>> {'key2': 'value2 from x', 'key3': 'value3 from y', 'key1': 'value1 from x'}

Приклад зараз:

x = {'key1': 'value1 from x', 'key2': 'value2 from x'}
y = {'key2': 'value2 from y', 'key3': 'value3 from y'}
x |= y

print(x)
print(y)

>>> {'key1': 'value1 from x', 'key2': 'value2 from y', 'key3': 'value3 from y'}
>>> {'key2': 'value2 from y', 'key3': 'value3 from y'}

2. Type hinting завдяки вбудованим колекціям

Тепер не потрібно імпортувати модуль typying, щоб додати тайпінг списків, діктів, сетів та інших вже вбудованих класів.

Приклад зараз:

from typing import List  # WHY?)

def print_all(names: list[str]) -> None:
    for name in names:
        print('Hello', name)

print_all(['Microsoft', 'Google', 'Apple'])

>>> Hello Microsoft
>>> Hello Google
>>> Hello Apple

3. .removeprefix() та .removesuffix() замість .startswith() чи .endswith()

Використовуєте .startswith() чи .endswith() методи рядка, щоб знайти збіг та видалити? Тепер у нас є можливісь робити це простіше новими методами .removeprefix() та .removesuffix().

Приклад зараз:

string = '12|34|56'

string = string.removeprefix('34')  # No effect
print(string)                       # 12|34|56

string = string.removeprefix('12')
print(string)                       # |34|56

string = string.removesuffix('56')
print(string)                       # |34|

string = string.removesuffix('34')  # No effect
print(string)                       # |34|

>>> '12|34|56'
>>> '|34|56'
>>> '|34|'
>>> '|34|'

Зміни у Python 3.10

Release Date: October 2021

1. Використання нового Union оператора `|`

У Python3.9 ми розглянули додавання built-in колекцій до type hinting. У Python3.10 отримали ще одне покращення — це використання нового Union оператора `|`. Якщо раніше ми вимушені були зазначити усі типи в Union:

Приклад раніше:

from typing import Union

def square(number: Union[int, float]) -> Union[int, float]:
    return number ** 2

То зараз ми зазначаємо типи, розбиваючи один з іншим оператором ‘|’:

Приклад зараз:

# Type hints can now be written in a more succinct manner:
def square(number: int | float) -> int | float:
    return number ** 2

Те саме стосується Optional, адже він являє собою Union[Type, None].

Приклад раніше:

from typing import Optional

def lower(value: Optional[str]) -> Optional[str]:
    return value.lower()

Приклад зараз:

def lower(value: str | None) -> str | None:
    return value.lower()

Можна зазначити, що mypy підтримує PEP-604.

2. Можливість використовувати контекстні менеджери з дужками

Зміна допоможе розбивати декілька колекцій, які користувачи передають в контекстний менеджер, на декілька рядків:

Приклад зараз:

with (open('file.txt') as file):
    ...

with (
    open('file1.txt'),
    open('file2.txt'),
):
    ...

with (
    open('file3.txt') as file3,
    open('file4.txt'),
):
    ...

with (
    open('file5.txt'),
    open('file6.txt') as file6
):
    ...

with (
    open('file7.txt') as file7,
    open('file8.txt') as file8,
):
    ...

Тепер це виглядає набагато симпатичніше, ніж перебирання в одному рядку через кому. Зазначимо, що підтримується кома в кінці (tralling coma).

3. Structural Pattern Matching

Не будемо обговорювати історичну складову, лише скажемо, що ця фіча прийшла до нас з багатьох інших мов, для прикладу Haskell, Scala, Rust або ж багато інших. Відразу зазначимо, що не варто плутати Pattern Matching із простим Match Case, адже це не просто оператор switch, як у C++. Pattern matching тут виступає і як механізм перевірки, і як розпаковки даних, а також здійснює керування потоком виконання.

Синтаксис

Приклад зараз:

match subject:
    case <pattern_1>:
        <action_1>
    case <pattern_2>:
        <action_2>
    case <pattern_3>:
        <action_3>
    case _:
        <action_wildcard>

Часто потрібно було будувати конструкції на кшталт цієї:

Приклад раніше:

def get_experienced_from_rank(rank):
    lower_rank = rank.lower()
    if lower_rank == 'student' or lower_rank == 'intern' or lower_rank == 'trainee':
        return f'{rank} is <1 year'
    elif lower_rank == 'junior':
        return f'{rank} is 1-3 years'
    elif lower_rank == 'middle':
        return f'{rank} is 3-5 years'
    else:
        return f'{rank} is unspecified'

print(get_experienced_from_rank('Trainee'))
print(get_experienced_from_rank('JUNIOR'))
print(get_experienced_from_rank('middle'))
print(get_experienced_from_rank('Senior'))

>>> Trainee is <1 year
>>> JUNIOR is 1-3 years
>>> middle is 3-5 years
>>> Senior is unspecified

Хтось скаже, що можна записати коротше, наприклад, виділивши список початківців до списку/кортежу — ОК:

Приклад раніше:

def get_experienced_from_rank(rank):
    lower_rank = rank.lower()
    if lower_rank in ('student', 'intern', 'trainee'):
        return f'{rank} is <1 year'
    elif lower_rank == 'junior':
        return f'{rank} is 1-3 years'
    elif lower_rank == 'middle':
        return f'{rank} is 3-5 years'
    else:
        return f'{rank} is unspecified'

print(get_experienced_from_rank('Trainee'))
print(get_experienced_from_rank('JUNIOR'))
print(get_experienced_from_rank('middle'))
print(get_experienced_from_rank('Senior'))

>>> Trainee is <1 year
>>> JUNIOR is 1-3 years
>>> middle is 3-5 years
>>> Senior is unspecified

Проте зараз можемо робити це простіше:

Приклад зараз:

def get_experienced_from_rank(rank):
    match rank.lower():
        case 'student' | 'intern' | 'trainee':
            return f'{rank} is <1 year'
        case 'junior':
            return f'{rank} is 1-3 years'
        case 'middle':
            return f'{rank} is 3-5 years'
        case _:
            return f'{rank} is unspecified'

print(get_experienced_from_rank('Trainee'))
print(get_experienced_from_rank('JUNIOR'))
print(get_experienced_from_rank('middle'))
print(get_experienced_from_rank('Senior'))

>>> Trainee is <1 year
>>> JUNIOR is 1-3 years
>>> middle is 3-5 years
>>> Senior is unspecified

Як бачимо, можна використовувати | («or»), а замість «default», ми використовуємо _ (wildcard). Він спрацює у тому випадку, коли усі інші кейси вище не підійшли. Зазначимо, що після цього блоку не можна використовувати інших case, інакше отримаєте помилку SyntaxError: wildcard makes remaining patterns unreachable.

Приклад зараз:

def get_experienced_from_rank(rank):
    match rank.lower():
        case 'student' | 'intern' | 'trainee':
            return f'{rank} is <1 year'
        case 'junior':
            return f'{rank} is 1-3 years'
        case _:
            return f'{rank} is unspecified'
        case 'middle':
            return f'{rank} is 3-5 years'

print(get_experienced_from_rank('Trainee'))

>>> File "incorrect-case-syntax.py", line 7
>>>    case _:
>>>         ^
>>> SyntaxError: wildcard makes remaining patterns unreachable

Чи будуть в майбутньому так писати? Перевіримо у найближчі роки, адже вибір старого чи нового запису, врешті-решт, залежить від користувачів.

Я ж казав, що це не все? :)

Також бувають випадки, коли користуачі не тільки перевіряють на рівність чомусь, а хочуть перевірити, чи вхідні дані є списком чи кортежем. У такому випадку аж ніяк не вистачає анотації, бо вона лише допомагає статичному аналізу, за допомогою лінтерів, середовищ розробки чи mypy, а на вхід у таку функцію і досі може потрапити будь-що. Після успішної перевірки, ми хочемо розпакувати та використати значення в тілі умови:

Приклад раніше:

def get_vector_length(vector: list | tuple):
    if isinstance(vector, list | tuple) and len(vector) == 1:
        x, = vector
        return x
    elif isinstance(vector, list | tuple) and len(vector) == 2:
        x, y = vector
        return math.sqrt(x**2 + y**2)
    elif isinstance(vector, list | tuple) and len(vector) == 3:
        x, y, z = vector
        return math.sqrt(x**2 + y**2 + z**2)
    else:
        raise ValueError

print(get_vector_length([3]))
print(get_vector_length([0, 3]))
print(get_vector_length([0, 4, 3]))

>>> 3
>>> 3.0
>>> 5.0

Так ми робили перевірку та розпакування у версіях раніше, проте для чого залишати цей спосіб, якщо Pattern Matching дозволяє робити дві дії одночасно:

Приклад зараз:

def get_vector_length(vector: list | tuple):
    match vector:
        case (x,):
            return x
        case (x, y):
            return math.sqrt(x**2 + y**2)
        case (x, y, z):
            return math.sqrt(x**2 + y**2 + z**2)
        case _:
            raise ValueError

print(get_vector_length([3]))
print(get_vector_length([0, 3]))
print(get_vector_length([0, 4, 3]))

>>> 3
>>> 3.0
>>> 5.0
Елегантно, чи не так? Можна придумати безліч кейсів використання, одним з них може бути також використання Pattern Matching з класами. Наведемо і такий приклад.

Опишемо класи, Vector2D, Vector3D:

Приклад зараз:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @property
    def length(self):
        return math.sqrt(self.x**2 + self.y**2)

class Vector3D:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    @property
    def length(self):
        return math.sqrt(self.x**2 + self.y**2 + self.z**2)

Створимо функцію, яка в матчер приймає екземпляр класу Vector2D чи Vector3D, та виведе інформацію про них, інакше виведемо «Another vector»:

Приклад зараз:

def get_vector_info(vector: Vector2D | Vector3D):
    match vector:
        case Vector2D(x=0 as x, y=0 as y):
            print(f'Zero Vector2D ({x=}:{y=})')
        case Vector2D(x=x, y=y):
            print(f'Vector2D ({x=}:{y=}). Len:{vector.length}')
        case Vector3D(x=0 as x, y=0 as y, z=0 as z):
            print(f'Zero Vector3D ({x=}:{y=}:{z=})')
        case Vector3D(x=x, y=y, z=z):
            print(f'Vector3D ({x=}:{y=}:{z=}). Len:{vector.length}')
        case _:
            print('Another vector')

get_vector_info(Vector2D(0, 0))
get_vector_info(Vector2D(0, 1))
get_vector_info(Vector3D(0, 0, 0))
get_vector_info(Vector3D(0, 3, 4))
get_vector_info([0, 3, 4])

>>> Zero Vector2D (x=0:y=0)
>>> Vector2D (x=0:y=1). Len:1.0
>>> Zero Vector3D (x=0:y=0:z=0)
>>> Vector3D (x=0:y=3:z=4). Len:5.0
>>> Another vector

Як бачимо, записи дуже схожі на виклик конструкторів Vector2D чи Vector3D, проте потрібно розуміти, що матчер не викликає конструкторів. Перевірка відбувається зарахунок співставлення параметрів. Ми навіть можемо перевіряти виключно значення x=0, коли як створення такого екземпляру приведе до отримання помилки TypeError:

Приклад зараз:

def get_vector_info(vector: Vector2D | Vector3D):
    match vector:
        case Vector2D(x=0 as x):
            print(f'Zero Vector2D ({x=})')
        case Vector2D(x=x, y=y):
            print(f'Vector2D ({x=}:{y=}). Len:{vector.length}')
        case Vector3D(x=0 as x):
            print(f'Zero Vector3D ({x=})')
        case Vector3D(x=x, y=y, z=z):
            print(f'Vector3D ({x=}:{y=}:{z=}). Len:{vector.length}')
        case _:
            print('Another vector')

get_vector_info(Vector2D(0, 0))
get_vector_info(Vector3D(0, 3, 4))
vector3d = Vector3D(0)

>>> Traceback (most recent call last):
>>>   File "match-vector.py", line 16, in <module>
>>>     vector3d = Vector3D(0)
>>> TypeError: Vector3D.__init__() missing 2 required positional arguments: 'y' and 'z'
>>> Zero Vector2D (x=0)
>>> Zero Vector3D (x=0)
Залишимо вам можливість експериментувати та знаходити свої способи використання цього оператора, проте ми чітко впевнені, що цей оператор буде частовживаним, завдяки лаконічності та мультифункціональності, за умови коректного використання.

Підсумки

Сучасна динаміка ринку потребує постійної актуалізації знань та швидкої відповіді на нові тренди. Ми впевнені, що найближчим часом нова функціональність оновить список best-practice, а це означає, що ви, як спеціаліст, зможете пришвидшити розробку та підтримку вашого коду. Дякую за прочитання. Слава Україні🇺🇦

Джерела:

👍ПодобаєтьсяСподобалось24
До обраногоВ обраному7
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

Коли вже впорядкують зоопарк з none, None, nan, NaN, None, pd.NA ?

воно впорядковано — правильно є None та math.nan

Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> None
>>> none
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'none' is not defined
>>> NaN
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'NaN' is not defined
>>> import math
>>> math.nan
nan
>>> math.NaN
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'math' has no attribute 'NaN'
>>> 

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

А, так ти побачив на картинці якісь букви і тебе це вразило?
Ну ок тоді.
Можеш пошукати інші картинки, там будуть інші букви

З нетерпінням чекаємо список нових можливостей в Python for Workgroups.

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

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