Оновлення Python 3.12 — чи справді мова тепер вдвічі швидша. Ключові зміни

Привіт! Мене звуть Олексій. Я студент першого курсу магістратури у Київському політехнічному інституті ім. Сікорського, і я програмую на Python уже близько пʼяти років. Це вже третя стаття із серії #найголовніші_фічі, які зʼявляються з новими версіями Python.

Поговоримо про зміни, бо Python міцно тримає лідерську позицію у рейтингу TIOBE Index for January 2024 & PYPL Popularity of Programming Language, залишаючи позаду Java, JS, та C/C++. У чому ж секрет?

Я розділив статтю на дві частини: практичну та теоретичну.

Перша (яку ви зараз читаєте) — основна. Тут ви дізнаєтеся про ключові зміни, які приніс нам реліз Python 3.12. Ви також познайомитеся з прикладами використання цих змін, довідаєтеся про їхні переваги та чи варто якнайшвидше переходити на нову версію.

У другій частині зосередимо більше уваги на порівнянні швидкодії (performance comparison) останніх версій: від інсталяції версій Python за останні п’ять років (версії 3.8-3.12) за допомогою pyenv, до запуску вимірювання швидкодії бенчмарок та порівняння цих результатів (посиланнячко на другу частину — наприкінці поточної).

Зміни у Python 3.12

Release Date: October 2023

Новий синтаксис параметра типів — type

Нагадаю, що в Python 3.5 (PEP 484) було внесено концепцію змінних типів. Розвиток цієї ідеї відбувся у PEP 612 (яке впровадило специфікації параметрів), і PEP 646 (яке розширило це поняття до змінних варіаційних типів).

Хоча generic-типи та параметри типів стали популярними, синтаксис визначення параметрів типів все ще виглядає дещо «прикріпленим» до Python. Це призводить до плутанини серед розробників Python.

Розгляньмо приклад з функцією, що відповідатиме за сортування (алгоритмом QuickSort) заданого списку, що складається з одного з елементів можливих типів int, float чи str:

def sort(iterable: list) -> list:
   """
    Sorts a list by using quicksort in ascending order.
    Parameters:
    - iterable (list[]): The input list to be sorted.
    Returns:
    - list[T]: The sorted list.
    """
    less = []
    equal = []
    greater = []
    if len(iterable) > 1:
        pivot = iterable[0]
        for x in iterable:
            if x < pivot:
                less.append(x)
            elif x == pivot:
                equal.append(x)
            elif x > pivot:
                greater.append(x)
        return sort(less) + equal + sort(greater)
    else:
        return iterable

Що є аналогічним для

from typing import Any
def sort(iterable: list[Any]) -> list[Any]:
    # Sorting realization

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

Наведу приклад використання:

int_list = [1, 0, 3, 2]
sort(int_list)
>>> [0, 1, 2, 3]
float_list = [1.0, 0.0, 3.0, 2.0]
sort(float_list)
>>> [0.0, 1.0, 2.0, 3.0]
str_list = ["1", "a", "2", "b", "3", "c"]
sort(str_list)
>>> ['1', '2', '3', 'a', 'b', 'c']

Досі в реалізації ми не показали можливі типи int, float та str. Ви можете подумати, що для цього достатньо перебрати їх у множині Union, як тут:

def sort(iterable: list[Union[int, float, str]]) -> list[Union[int, float, str]]:
    # Sorting realization

Чи, як я писав у статті про нові можливості Python 3.10, за допомогою використання нового Union оператора |:

def sort(iterable: list[int | float | str]) -> list[int | float | str]:
    # Sorting realization

Але ні, у цьому випадку ми дозволимо передавати на вхід списки з різними типами:

different_list = [1, 1.0, "1"]

Це призведе до помилки типізатора:

error: Unsupported operand types for < ("int" and "str")  [operator]
error: Unsupported operand types for < ("float" and "str")  [operator]
error: Unsupported operand types for < ("str" and "int")  [operator]
error: Unsupported operand types for < ("str" and "float")  [operator]
note: Both left and right operands are unions
error: Unsupported operand types for > ("int" and "str")  [operator]
error: Unsupported operand types for > ("float" and "str")  [operator]
error: Unsupported operand types for > ("str" and "int")  [operator]
error: Unsupported operand types for > ("str" and "float")  [operator]
note: Both left and right operands are unions
error: Argument 1 to "sort" has incompatible type "list[int]"; expected "list[int | float | str]"  [arg-type]
note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
note: Consider using "Sequence" instead, which is covariant
error: Argument 1 to "sort" has incompatible type "list[float]"; expected "list[int | float | str]"  [arg-type]
note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
note: Consider using "Sequence" instead, which is covariant
error: Argument 1 to "sort" has incompatible type "list[str]"; expected "list[int | float | str]"  [arg-type]
note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
note: Consider using "Sequence" instead, which is covariant
Found 11 errors in 1 file (checked 1 source file)

Ми прагнемо показати, що список елементів з типом на виході функції відповідає типу, з елементів яких складається параметр списку на вході функції. Для порівняння, всередині алгоритму ми також будемо використовувати елементи однакового типу. Для цього нам потрібно використати параметризований параметр, TypeVar.

from typing import TypeVar
T = TypeVar("T")
def sort(iterable: list[T]) -> list[T]:
    # Sorting realization

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

Аналіз 25 популярних типізованих бібліотек Python показав, що типові змінні (зокрема, символ typing.TypeVar) використовуються у 14% модулів.

У Python 3.12 ми отримали нову можливість представити аналогічну функцію — тільки вже без оголошення TypeVar:

def sort[T](iterable: list[T]) -> list[T]:
    # Sorting realization

*Застереження. Якщо отримали схожу помилку:

  File "/home/oleksii/python/example.py", line 1
    def sort[T](iterable: list[T]) -> list[T]:
            ^
SyntaxError: expected '('

Спробуйте перевірити, чи точно ви здійснюєте виконання свого коду з Python 3.12 😉.

На момент тестування, mypy ще не підтримує цей синтаксис. Тому поки що отримуємо помилку:

error: PEP 695 generics are not yet supported  [valid-type]
error: Name "T" is not defined  [name-defined]

Проте після додавання підтримки даного синтаксису до всіх статичних типізаторів ви зможете отримати всі переваги цього лаконічного запису.

Для повного завершення поставленої задачі ми мусимо ще дописати до T bounds типів int, float та str.

T = TypeVar("T", int, float, str)

Повторити це для нового синтаксису ви зможете за допомогою:

def sort[T: (int, float, str)](iterable: list[T]) -> list[T]:
    # Sorting realization

Тепер ви більше не потребуватимете імпортування та ручної ініціалізації TypeVar. Крім того, ви зможете також використовувати цей спосіб застосування з декількома типами і класами. Розгляньмо приклад використання з останніми:

class SortedList[T: (int, float, str)]:
    def __init__(self, iterable: list[T]) -> None:
        self.sorted_list = sort(iterable)
    def get_sorted_list(self) -> list[T]:
        return self.sorted_list
int_list = [1, 0, 3, 2]
int_sorted_list = SortedList[int](int_list)
int_sorted_list.get_sorted_list()

Проте я і досі не розповів про новий синтаксис параметра типів — type. Відтепер ми можемо створювати псевдоніми типів. Для оголошення використовується інструкція type:

type Item = dict[int, int | float | str] | dict[str, str]

У випадку, коли нам потрібно описати вкладену структуру з багатьма можливими варіантами застосування множиного оператора | або Union, це дуже зіграє нам на руку. Адже варто уникати багаторазового використання таких обʼємних записів у код базі, коли є можливість використовувати псевдоніми.

Синтаксична формалізація f-рядків

У Python 3.12 з’явилися чудові покращення для f-рядків, що забезпечують більшу гнучкість та виразність форматування рядків. Покращення формалізовано у PEP 701, який вносить синтаксичні зміни до f-рядків. Це знімає попередні обмеження і робить f-string ще потужнішими.

У Python 3.11 повторне використання тих самих лапок, що й укладений в них f-рядок, викликало синтаксичну помилку. Це обмежувало можливості вкладеності.

Приклад:

import random
random_choice = random.choice([True, False])
motto = f'I like to read Tolkien\'s {'Hobbit' if random_choice else 'The Lord of the Rings'}!'
motto
>>>   File "/home/oleksii/python/example.py", line 4
>>>     motto = f'I like to read Tolkien\'s {'Hobbit' if random_choice else 'The Lord of the Rings'}!'
>>>                                                         ^^^^^^
>>> SyntaxError: f-string: expecting '}'

Для подолання цієї проблеми потрібно було використовувати інші лапки. Наприклад:

motto = f"I like to read Tolkien\'s {'Hobbit' if random_choice else 'The Lord of the Rings'}!"
motto
>>> I like to read Tolkien's The Lord of the Rings!

Спосіб маркувати кожен вкладений символ лапок символом \ (як це показано з Tolkien\’s) не працював:

motto = f'I like to read Tolkien\'s {\'Hobbit\' if random_choice else \'The Lord of the Rings\'}!'
>>>   File "/home/oleksii/python/example.py", line 4
>>>     motto = f'I like to read Tolkien\'s {\'Hobbit\' if random_choice else \'The Lord of the Rings\'}!'
>>>                                                                                                       ^
>>> SyntaxError: f-string expression part cannot include a backslash

Спосіб не спрацював, бо f-рядки зовсім не могли містити зворотні слеші, як символ \n для переведення на новий рядок, чи символи Юнікоду.

У Python 3.12 використовувати зворотні слеші для позначення внутрішніх лапок і далі не можна, проте у цьому тепер немає потреби. Обмеження на символи Юнікоду та використання тих самих лапок, що позначають f-рядки, було знято. Це дозволяє більш виразно використовувати f-рядки:

import random
random_choice = random.choice([True, False])
motto = f'I like to read Tolkien\'s {'\nHobbit' if random_choice else '\nThe Lord of the Rings'}!'
motto
>>> I like to read Tolkien's 
>>> The Lord of the Rings!

Окрім того, тепер f-рядки можна вкладати довільно, що забезпечує більшу гнучкість:

f'{f'{f'The Lord of' + f'the Rings!'}'}'  # Missed space here!
>>> The Lord ofthe Rings!

Також зʼявилась можливість багаторядкових виразів та коментарів.

Python 3.12 дозволяє визначати f-рядки у декількох рядках і включає вбудовані коментарі для покращення читабельності:

f'{  # First comment
    f'{  # Second comment
        'The Lord of' + 'the Rings!'  # Missed space here!
    }'
}'
>>> The Lord ofthe Rings!

Ці зміни дозволяють створювати більш виразні та універсальні f-рядкові вирази.

Разом із цим покращенням ми отримали точніше повідомлення про помилку.

Python 3.12 містить точніші повідомлення про помилки для f-рядків завдяки синтаксичному аналізатору PEG. На відміну від Python 3.11:

text = f'{1 2 3}'
>>>   File "/home/oleksii/python/example.py", line 1
>>>     (1 2 3)
>>>      ^^^
>>> SyntaxError: f-string: invalid syntax. Perhaps you forgot a comma?

Повідомлення тепер показують точне місце помилки в рядку:

text = f'{1 2 3}'
>>> Traceback (most recent call last):
>>>   File "<frozen runpy>", line 189, in _run_module_as_main
>>>   File "<frozen runpy>", line 112, in _get_module_details
>>>   File "/home/oleksii/python/example.py", line 1
>>>     text = f'{1 2 3}'
>>>               ^^^
>>> SyntaxError: invalid syntax. Perhaps you forgot a comma?

Крім того, завдяки змінам в PEP 701 генерація токенів за допомогою модуля tokenize прискорюється на 64%.

GIL для кожного інтерпретатора. Шлях до паралелізму

Глобальний блокувальник інтерпретатора (GIL) у Python традиційно обмежував одночасне виконання потоків, ускладнюючи роботу з паралельним кодом. PEP 684 вносить кардинальні зміни, дозволяючи кожному субінтерпретатору мати власний GIL. Це забезпечує краще використання потужностей багатоядерних процесорів.

Для створення субінтерпретатора з власним GIL розробники можуть скористатися функцією Py_NewInterpreterFromConfig(), як показано у прикладі. Можливість створювати підінтерпретатори з окремими GIL наразі доступна через C-API, а з версії Python 3.13 очікується поява і у API для Python.

З окремим GIL для кожного інтерпретатора розробники Python отримують гнучкість у дослідженні нових моделей паралелізму. Під капотом відбувся рефакторинг внутрішніх компонентів CPython. Ініціатива передбачала перенесення більшої частини глобального сховища станів до сховища для кожного інтерпретатора.

Попри відсутність суттєвих змін, версія Python 3.12 водночас і не змінює поточних підходів, і є підготовкою до великих покращень, бо закладає основу для майбутнього розвитку паралелізму.

Покращення повідомлень помилок NameError, ImportError і SyntaxError

Ми вже розглядали, як з кожною версією Python розробники ядра покращували вивід помилок та процес знаходження помилок. Завдяки цьому знаходження помилок у ваших програмах стає очевиднішим і простішим.

У Python 3.10 повідомлення про помилки, особливо пов’язані з синтаксисом, стали більш інформативними та точними.

У Python 3.11 були розширені можливості трасування, що полегшило пошук проблемного коду.

Останній випуск, Python 3.12, продовжує цю тенденцію до покращення досвіду розробників, надаючи більш інформативні повідомлення про помилки. Зокрема деякі поширені повідомлення про помилки тепер супроводжуються корисними підказками. Розгляньмо детальніше ці нові й поліпшені повідомлення.

Значна частина цих покращень стосується процесу імпорту модулів. Уявімо сценарій, у якому ми будемо працювати зі змінними оточення, дістаючи їх з модуля os, імпортуючи його перед цим.

3.11:

os.getenv("PROJECT_NAME")
>>> Traceback (most recent call last):
>>>   File "/home/oleksii/python/example.py", line 1, in <module>
>>>     os.getenv("PROJECT_NAME")
>>>     ^^
>>> NameError: name 'os' is not defined
3.12:
os.getenv("PROJECT_NAME")
>>> Traceback (most recent call last):
>>>   File "<stdin>", line 1, in <module>
>>> NameError: name 'os' is not defined. Did you forget to import 'os'?

Під час спроби використати os без його попереднього імпорту виникає традиційна помилка виклику NameError. Однак у Python 3.12 з’явилося корисне нагадування, яке радить вам імпортувати модуль os перед спробою отримати до нього доступ.

Це нагадування про імпорт стосується лише стандартних бібліотечних модулів і не відстежує сторонні бібліотеки, які ви могли встановити.

Ще одне покращення тексту в інформації помилки також стосується імпорту. Тепер, якщо ви випадково поміняєте порядок ключових слів в інструкції from-import:

Python 3.11:

import getenv from os
>>>   File "/home/oleksii/python/example.py", line 1, in <module>
>>>     import getenv from os
>>>                   ^^^^
>>> SyntaxError: invalid syntax

Python 3.12 дружелюбно підштовхне вас до правильного синтаксису:

import getenv from os
>>>   File "<stdin>", line 1
>>>     import getenv from os
>>>     ^^^^^^^^^^^^^^^^^^^^^
>>> SyntaxError: Did you mean to use 'from ... import ...' instead?

У цьому прикладі спроба імпортувати getenv з os модуля викликає синтаксичну помилку. Не впевнений, що вона поширена серед досвідчених розробників, проте Python 3.12 виявляє її і пропонує використовувати правильний синтаксис з from ... import .....

Ще одне покращення повідомлення про помилку стосується імпорту конкретних імен з модуля.

Python 3.11:

from os import get_env
>>>   File "/home/oleksii/python/example.py", line 1
>>>     >>> Traceback (most recent call last):
>>>     ^^
>>> SyntaxError: invalid syntax

Python 3.12:

from os import get_env
>>> Traceback (most recent call last):
>>>   File "<stdin>", line 1, in <module>
>>> ImportError: cannot import name 'get_env' from 'os' (/home/oleksii/.pyenv/versions/3.12.1/lib/python3.12/os.py). Did you mean: 'getenv'?

У цьому випадку спроба імпортувати get_env з os призводить до помилки імпорту. Python 3.12 припускає, що ви, ймовірно, хотіли імпортувати getenv замість get_env. Ця функція, запроваджена в Python 3.10, тепер поширюється і на операторів імпорту в Python 3.12.

Окрім покращень, пов’язаних з імпортом, у Python 3.12 впроваджене також покращення методів, визначених усередині класів.

Розгляньмо для прикладу реалізацію класу Person:

class Person:
    def __init__(self, name: str):
        self.name = name
    def print_name(self):
        print(name)

Python 3.11:

Person("Oleksii").print_name()
>>> Traceback (most recent call last):
>>>   File "/home/oleksii/python/example.py", line 141, in <module>
>>>     Person("Oleksii").print_name()
>>>   File "/home/oleksii/python/example.py", line 138, in print_name
>>>     print(name)
>>>           ^^^^
>>> NameError: name 'name' is not defined

Python 3.12:

Person("Oleksii").print_name()
>>> Traceback (most recent call last):
>>>   File "<stdin>", line 1, in <module>
>>>   File "<stdin>", line 5, in print_name
>>> NameError: name 'name' is not defined. Did you mean: 'self.name'?

Замість звичайного NameError, Python 3.12 розпізнає, що name є атрибутом, доступним на self, і пропонує використовувати атрибут екземпляра self.name замість локального імені name.

Buffer-протокол

У Python 3.12 впроваджене PEP 688, що робить буферний протокол доступним безпосередньо з коду Python.

Це покращення дозволяє класам, що реалізують метод __buffer__(), використовувати буферні типи. Крім того, доданий абстрактний клас collections.abc.Buffer (ABC), що стандартизує представлення буферних об’єктів, полегшуючи їх використання в анотаціях типів.

Мотивують PEP 688 тим, що це має допомогти подолати розрив між буферним протоколом Python та C.

Буферний протокол, який спочатку був доступний лише для коду на C, тепер отримав API на рівні Python. Ця розробка дозволяє засобам статичної типізації оцінювати, чи об’єкти реалізують протокол, та усуває обмеження поточних опцій.

Наразі в коді Python відсутній механізм перевірки того, чи підтримує об’єкт буферний протокол. У статичних типізаторах також відсутня спеціальна анотація типу для представлення протоколу.

Це стає проблемою під час написання анотацій типів для коду, який приймає загальні буфери. Є два шляхи використання псевдоніму типу для відомих типів буферів та використання typing.ByteString, але їх вважають неадекватними з різних причин.

Буферний протокол C підтримує різні можливості, пов’язані з кроками, неперервністю та підтримкою запису у буфер. Хоча typeshed надає псевдоніми типів для буферів, доступних для запису та читання, багато з цих опцій не можуть бути безпосередньо запитані в об’єкті типу в буферному протоколі C.

Практичний приклад з використанням collections.abc.Buffer, що наводить документація:

def need_buffer(b: Buffer) -> memoryview:
    return memoryview(b)
need_buffer(b"xy")  # ok
need_buffer("xy")  # rejected by static type checkers

Також це може бути використане з isinstance та issubclass перевірками:

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:

import contextlib
import inspect
class MyBuffer:
    def __init__(self, data: bytes):
        self.data = bytearray(data)
        self.view = None
    def __buffer__(self, flags: int) -> memoryview:
        if flags != inspect.BufferFlags.FULL_RO:
            raise TypeError("Only BufferFlags.FULL_RO supported")
        if self.view is not None:
            raise RuntimeError("Buffer already held")
        self.view = memoryview(self.data)
        return self.view
    def __release_buffer__(self, view: memoryview) -> None:
        assert self.view is view  # guaranteed to be true
        self.view.release()
        self.view = None
    def extend(self, b: bytes) -> None:
        if self.view is not None:
            raise RuntimeError("Cannot extend held buffer")
        self.data.extend(b)
buffer = MyBuffer(b"capybara")
with memoryview(buffer) as view:
    view[0] = ord("C")
    with contextlib.suppress(RuntimeError):
        buffer.extend(b"!")  # raises RuntimeError
buffer.extend(b"!")  # ok, buffer is no longer held
with memoryview(buffer) as view:
    assert view.tobytes() == b"Capybara!"

Підвищення продуктивності isinstance та asyncio

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

У Python 3.12 функція isinstance() для перевірок протоколів, що перевіряються під час виконання, має помітні покращення під час перевірки протоколів. Більшість перевірок isinstance() протоколів мають бути принаймні удвічі швидшими, ніж у версії 3.11.

Деякі можуть бути навіть у 20 разів швидшими, або і більше. Разом з тим, перевірка isinstance() протоколів з чотирнадцятьма або більше членами може, навпаки, бути повільнішою, ніж у Python 3.11.

Пакет asyncio в Python 3.12 також зазнав значних покращень продуктивності. Це призвело до збільшення швидкості на 75%, у чому ми з вами спробуємо переконатись у другій частині цієї статті, де ми позапускаємо різні бенчмарки та порівняємо зміни швидкодії.

Використання TypedDict для точнішої анотації **kwargs

Розгляньмо PEP 692 — першу зміну, яка стосується анотації. Тепер ми маємо змогу більш детально описувати **kwargs. **kwargs — спеціальний синтаксис, який дозволяє передавати до функції словник аргументів змінної довжини з ключовими словами. Це скорочення від keyword-arguments (аргументи ключового слова).

Раніше, коли ми передавали **kwargs як словник з ключами типу str та будь-якими значеннями, ми могли як максимум тільки скористатись конструкцією **kwargs: Any. Або, якщо щастило, **kwargs: dict[str, Any]. Проте зараз ми можемо оголосити TypedDict, та використовуючи typing.Unpack, явно вказати, що очікується на вході **kwargs.

Приклад:

from typing import TypedDict, Unpack
class Book(TypedDict):
    title: str
    author: str
    publication_year: int
def process_book_info(**kwargs: Unpack[Book]) -> str:
    title = kwargs['title']
    author = kwargs['author']
    publication_year = kwargs['publication_year']
    book_info = f"Title: {title}\nAuthor: {author}\nYear of Publication: {publication_year}"
    # Можлива якась додаткова логіка обробки
    return book_info
lotr_info: Book = {"title": "The Lord of the Rings", "author": "J.R.R. Tolkien", "publication_year": 1954}
result = process_book_info(**lotr_info)
result
>>> Title: The Lord of the Rings
>>> Author: J.R.R. Tolkien
>>> Year of Publication: 1954

Ця можливість уже підтримується типізаторами, а ось під час запуску на версії Python 3.11 ми отримаємо таку помилку:

error: "Unpack" support is experimental, use --enable-incomplete-feature=Unpack to enable  [misc]

@override для явного перевизначення методів

Другою новинкою для анотації типів у Python 3.12 став декоратор @override — цікавий спосіб типізації, призначений для явного позначення методів у підкласах, які перевизначають їхні аналоги у батьківському класі. Це вдосконалення є подібним до механізмів в таких мовах, як-от Java та C++.

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

Знаю точно, що цей декоратор (як і @abstractmethod для батьківського) посилює зв’язку між класами та допомагає розробникам створювати більш надійний та стійкий до помилок код за допомогою аналізу інструментами статичної типізації.

Приклад використання:

from typing import override
class BaseCase:
    def __init__(self, text: str):
        self.text = text
    def print_color(self):
        print(self._get_text())
    def _get_text(self) -> str:
        return self.text
class UpperCase(BaseCase):
    @override
    def _get_text(self) -> str:
        return self.text.upper()
class LowerCase(BaseCase):
    @override  # Помилка Missing super method for override. У назві пропущено "_" префікс
    def get_text(self) -> str:
        return self.text.lower()

Підсумки

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

  1. Новий синтаксис параметра типів — type.
  2. Синтаксичну формалізацію f-рядків.
  3. GIL для кожного інтерпретатора.
  4. Покращення повідомлень помилок NameError, ImportError, і SyntaxError.
  5. Buffer-протокол.
  6. Підвищення продуктивності isinstance та asyncio.
  7. Використання TypedDict для точнішої анотації **kwargs.
  8. @override для явного перевизначення методів.

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

Якщо збираєтесь оновлюватись і використовуєте один з них, зверніть увагу на список модулів що зазнав змін: asynchat, asyncore, configparser, distutils, ensurepip, enum, ftplib, gzip, hashlib, importlib, imp, io, locale, smtpd, sqlite3, ssl, unittest, webbrowser, xml.etree.ElementTree, zipimport та інші.

Впевнено сказати, що ви мусите оновлювати ваші проєкти до 3.12, я не можу. Зміни сильно вплинули на деякі частини розробки, торкнулись покращення знаходження помилок, додали можливість кожному інтерпретатору окремо працювати з GIL.

Це важливо, бо закладає фундамент для паралелізму в нових версіях. Гвідо ван Россум (автор мови Python) обіцяв, що Python 3.11 буде вдвічі швидшим. Чи вдалося йому дотриматися обіцянки?

На це питання шукайте відповідь у другій частині статті. Там я встановлю паралельно останні версії Python та покажу як це зробити, використовуючи pyenv. Ми також запустимо pyperformance та займемось порівнянням швидкодії (performance comparison) останніх версій Python.

Якшо ви ще не читали попередні випуски, або бажаєте освіжити знання, нижче ви знайдете посилання на велику статтю, де я розібрав зміни Python з версії 3.8 до версії 3.10 та посилання на статтю зі змінами Python 3.11:

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

Джерела:

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

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

buffer protocol це мається на увазі google protobuff ?

GIL для кожного інтерпретатора.

nit pick: мова скоріше за все йде про субінтерпретатор.
До речі: хіба при роботі з multiprocessing GIL шариться між процесами?

Сьогодні DOU перейшов на Python 3.12

зробите порівняння швидкодії?
з даними за тиждень, наприклад.

Ось попередні результати (перехід з 3.10 до 3.12 позначено вертикальною червоною лінією), графік за останні 30 днів:
i.imgur.com/CxpA6Y1.png

виглядає так, шо швидкодія дійсно підросла 👍

Блин. Причем тут язык до скорости работы ИНТЕРПРЕТАТОРА?
Мова как была так и осталася.
А по синтаксису — ну просто джавистам джава жмет, они приходят в Питон и делают из него Джаву.
Но мы тут ничего не сделаем, ибо индийский консенсус — он такой.

Блин. Причем тут язык до скорости работы ИНТЕРПРЕТАТОРА?

Наверно, язык влияет на эту скорость своими решениями?
Или вы о чём вообще?

А по синтаксису — ну просто джавистам джава жмет, они приходят в Питон и делают из него Джаву.

По-вашему это плохо, что в нём наводят порядок и создают возможность лучше контролировать соответствие кода желаемому — в статике?

То есть, никакой многопоточности в Пайтоне как не было, так и не будет. То, что они запускают отдельные инстансы виртуальной машины с глобальным мьютексом в каждой — это не решение проблемы. Ровно то же самое делалось в Lua, причём изначально, а не спустя годы развития. С тем же успехом можно ещё несколько инстансов приложения запускать, и рассказывать про параллелизм.

А не пофиг, если все ядра проца поюзали? Да пускай там хоть фибры будут под капотом.

А синхронизировать данные между инстансами — через базу данных, или через сокеты между инстансами. Хотя какие там сокеты, через базу данных естественно. Ну и соответственно архитектура приложения должна исходить из концепции не-монопольного юзания БД, с частым чтением/записью туда на каждый чих. Ну в общем, что я тут прописные истины рассказываю, все именно так всегда и делают.

Для высоконагруженного есть shared nothing, но кто же заморачивается с месседжами...

Файберы как раз GIL и портит, для них нужен пул потоков под капотом. Пул по определению выполняет один роутин. т.е. один поток за раз из-за GIL если идея была задействовать 8 ядер на процессоре чтобы ускориться, то выходит что из 8 задействованных ядер 7 получили семафор и стоят пока одно отработает. Иначе получится состояние гонки самого интерпретатора. Решить можно двумя способами или клонировать форкать весь контекст интерпретатора или на старте программы включить JIT и весь код сразу скомпилировать в машинный код. Для AI/ML похоже что первый подход существенно предпочтительнее ибо нужна интерактивная среда аля MatCad, MatLab и т.п. для экспериментальных внесений изменений в код, типа добавочных нейронов смещения и т.д. и т.п. . Иначе бы просто на С++ писали, на фреймверках типа Сaffe.

Пока что конкретный костыль под AI/ML фреймверки типа NymPy, Tensorflow и PyTorch.

С тем же успехом можно ещё несколько инстансов приложения запускать, и рассказывать про параллелизм.

Это и будет параллелизм, но по памяти от потоков и пулов потоков конечно будет совсем не то пальто. В UNIX системах очень долго это был единственный вариант.
Смотря как стоит задача — если задача ускорить линейную алгебру и операции с матрицами и тензорами с помощью SIMD — то да, костыль очень плохой. И вообще тут нужно делать так чтобы сразу код уходил на GPU или спец асики, аля OpenMP — а еще лучше чтобы это вообще делалось оптимизатором без вмешательства программиста в процесс.
Конечно все это отходит от первоначального скриптового дизайна языка. За простоту использования его выбрали в университетах для специалистов по статистике и искусственному интелекту, чтобы они не заморачивались с программированием — а концентрировались на бизнес и научных задачах, аля MatCad, MatLab и т.п. идея. Вышло все наоборот — чем дальше в лес тем толще партизаны.

Мене доречі завжди приколювало типу Python — існує із 1991го — актуальна версія 3.12.2
NodeJS існує з 2009 — актуальна версія 21.6.1

Python підкупляє своєю стабільністю

Перший компілятор з С++ Cfront — 1984 рік. LLVM Сlang компілятор з С++ — перший реліз 2007 рік. SpiderMonkey рушій JavaScript — перший реліз 1995 рік, V8 / NodeJS — перший реліз 2009 рік.
Цей світ повний парадоксів.

$ less --version
less 551 (GNU regular expressions)
Copyright (C) 1984-2019  Mark Nudelman
Хто більше? ;)

AWK — 1977 один з авторів Брайан Керніган, давав інтервью ДОУ dou.ua/...​kernighan-on-programming
Досі активно використовується, останній реліз GNU AWK Вересень 2022 року.
Але ще є FORTRAN перший реліз 1957, Intel компілятор версія 2024.0.2 є ще GCC теж свіжий та купа інших компіляторів типу Portland Group . Міжнародний стандарт ISO/IEC 1539-1:2023 оновлено 17 листопада 2023.
На ньому розроблено купу усього зокрема NASTRAN від 1960 року розробник NASA, досі використовується і є сучасні CAE на його базі NX CAE наприклад з рушієм NASTRAN. Зокрема використовувався в проектуванні Сатурн 5 і польоту на Луну, Союз-Аполон і т.д.
BTW SIMD, підтримка багатопоточності, шаблони — вони же дженерики, модулі рівно як і купа бібліотек з лінійної алгебри та іншої математики — усе це у фортран завезли з новими версіями стандарту. В Python або JavaScript — ні :)

Кстати да, FORTRAN из всего программного обеспечения что сейчас существует — самый старый, и до сих пор ограниченно используется. В то время как про его ровесника Algol все уже забыли. С другой стороны, на его основе появился Pascal и все остальные языки, что мы сейчас используем.

Algol 66 и Algol 68 лет на 10 старше. Однако самый распространенный язык программирования среди всех по написанным на нем программах C — происходит имеено из Algol 68, который попал в Британию там стал BCPL и снова приехал в Америку :) Из С в свою очередь выходит тоже куча языков, включая JavaScript например. Причина упадка Algol многими историками ИТ бизнеса называю его европейское происхождение. В 80 американцы всеобщее захватили все лидирующие позиции в индустрии и люди спользовали то что поставляли фирмы которые поставляли компьютеры. IBM всегда поставлял FORTRAN и PL/1. Microsoft — BASIC и C/C++. SUN, SGI HP, DEC и прочее компании с UNIX стратегией — С. Постоянно выделываются только Apple, которые делают все возможное для не совместимости и непереносимости программ разработанных для их архитектур, это осознанная бизнес стратегия технологической конкуренции. У них то Object Pascal, то Objective C, теперь вот Swift.

Algol 68, который попал в Британию там стал BCPL

При этом в нём настолько всё урезали, что от того Algol 68 не осталось аж ничего.

Причина упадка Algol многими историками ИТ бизнеса называю его европейское происхождение.

Нет, то, что в таком виде никто просто не осилил на тогдашних мощностях сделать работающий компилятор.

Собственно тогда откат пошёл к более простым языкам (Pascal, C...) именно потому, что поняли, что старые методы не работают.

Связи с Европой тут нет.

Попробуйте найти вот сейчас известные и финансово успешные не американские средства разработки. Пока в общем то JetBrains поставляет такие, а в целом полная американшина, теже питерцы и иже с ними которые в Праге и Мюнхене давно, тоже работают под американцев в частности под Microsoft. Разные Ruby — на уровне стат погрешности.
Компиляторы с Algol 68 были, но они никому толком были не нужны, в отличие от скажем PL/1 или FORTRAN вместе с компьютерами конструкции : IBM или DEC они на рынок не поставлялись.

Попробуйте найти вот сейчас известные и финансово успешные не американские средства разработки.

С 1970 года сколько лет прошло? Тогда ещё толком Кремниевой Долины не было, а сейчас в ней уже стагнация и массовое бегство новых фирм.

Компиляторы с Algol 68 были

Малого подмножества от языка. Самые яркие и безумные фичи не реализовывались. Но и эти урезанные версии были чудовищно тяжелы.
Только в 74-м кто-то что-то сделал (забагованное). К тому времени C уже начал расползаться по миру.

но они никому толком были не нужны,

Именно. Он опоздал, и мог бы выйти только за счёт киллер-фич, которых не было.

AWK — 1977
Але ще є FORTRAN перший реліз 1957

Я казав про нумерацію версій програми.
З 1957 компілятори Fortran декілька разів перероблялись з нуля.
GNU awk теж не має жодного спільного рядку коду з початковим Bell awk.

BTW SIMD, підтримка багатопоточності, шаблони — вони же дженерики, модулі рівно як і купа бібліотек з лінійної алгебри та іншої математики — усе це у фортран завезли з новими версіями стандарту. В Python або JavaScript — ні :)

Фортранівські бібліотеки підключаються до Python через scipy, tensorflow і тому подібне. Наприклад, більшість реалізацій scipy компілюється саме з фортранівським BLAS (хоча OpenBLAS на C і C++ вже відʼїв помітну частку).
Про JS не знаю, хоча впевнений що в NodeJS є засоби для C FFI.
Дженерики в Фортрані були ще з 60-х, але не на класах (класи додали в версії 95).

Та звісно усі інтерпретатори мають можливості викликати нативний код, при чому в Python та Node дуже не погано зроблено навідміну від JNI в Java наприклад. Так само як ассемблерний код можна злінкувати хоч із C хоч із Pascal хоч з FORTRAN. Що до BLAS — рекомендую глянути на clBLAST. Справа не в цьому — є добрий дизайн який живе десятиліттями, є компромісний дизайн який усі лають і живе десятиліттями і є щось круте і експериментальне типу Simula чи Small Talk чим ніхто не користується, бо на практиці не підходило.
Python вже через проблеми з дизайном раз переробляли на корню, до стану зворотньої несумісності. JavaScript теж постійно щось переробляють. Java починаючи з 9-ї версії постійно втрачає зворотню сумісність і т.д.
Що С що FORTRAN — хоча і мають високу трудомісткість програмуванні тим не меньше ближче до реального заліза і зворотню сумісність не втрачають десятиліттями.

С++ також не втрачає — там кожну проблему з дизайном фіксять додаванням кількох нових

так за такою логікою і пітон пролітає,
інтерпретатор пітона перероблявся мінінум 2 рази
пітон 2 ніфіга не сумісний з пітон 3.)

так за такою логікою і пітон пролітає,

Мабуть да... а що? ;)

Пітон потроху перетворюється на С++ поки С++ перетворюється на Перл.

Больше похоже на поддержку больших проектов — жесткая типизация как у компилируемого языка. Шаблоны вот добавили

def sort[T](iterable: list[T]) -> list[T]:

Ну так же і виходе, скриптова мова яка задумана автором як проста у використанні але складна в реалізації — стає складна як в реалізації так і в використанні. А нововведення в С++ робляться з ціллю виправлення попередньо введених нововведень :) Наприклад code_cvt чи std::to_chars habr.com/ru/articles/144632
Щодо легкості прострелити собі ногу цілком за допомогою С++, чи наробити як шедевр так і кроваве місиво наголошував сам Бйорн Страуструп. Навідміну від Perl на С++ ніхто не заставляє перетворювати програму на тотальний regex, який через тиждень не може зрозуміти вже і сам автор кода.

Є два типа мов програмування — ті на які люди весь час лаяться і ті які ніхто не використовує Бйорн Страуструп.

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