Краткая история Python

Python был разработан около тридцати лет назад, первая его официальная версия вышла в 1991 году и имела номер 0.9. После этого довольно долгое время язык развивался, и популярность он получил в 2000-х годах, во время мажорной второй версии. Однако уже тогда было понятно, что в языке необходимы изменения, которые будут обратно несовместимы с текущими версиями, такие как например, изменения в поведении юникодных строк. Однако вторая версия Python была уже очень популярна к этому моменту, поэтому третья работа над третьей версией велась одновременно с работой над 2.7. Довольно долго у core-разработчиков Python не было определенной стратегии, поэтому до версии 3.4 изменения были довольно хаотичны.

python_versions

С 2019 года Python адаптирует годичные релизные циклы. Это изменение было представлено в PEP-602 Что это значит для пользоватеоей Python?

  • новая минорная (3.X.0) версия выходит каждый год
  • фаза активной разработки версии 3.X+1.0 начинается в тот момент, когда в релих отправляется 3.X.beta и продолжается 12 месяцев
  • каждая минорная версия активно поддерживается в течении полутора лет
  • каждая минорная версия получает обновления безопасности в течении трех с половиной лет Пример расписания релизов для версии 3.9 можно посмотреть на рисунке:
release_calendar

What’s new in Python 3.9?

Релиз Python 3.9 произошел 2020-10-05. Рассмотрим что есть нового в этой версии.

Работа со словарями

Допустим, есть два словаря, которые мы зотим объединить.

В версиях до 3.9 можно было бы сделать таким образом:

In [9]:

pycon = {2017: "Portland", 2018: "Cleveland", 2019: "Cleveland", 2020: "online"}
europython = {2017: "Rimini", 2018: "Edinburgh", 2019: "Basel"}

{**pycon, **europython}

Out[9]:

{2017: 'Rimini', 2018: 'Edinburgh', 2019: 'Basel', 2020: 'online'}

Синтаксис ** разворачивает словарь, и когда они объединяются, более позднее значение затирается более новым. Аналогичные дейтсвия можно совершить, если использовать следующий синтаксис:

In [7]:

merged_dict = pycon.copy()
for key, value in europython.items():
    merged_dict[key] = value
merged_dict

Out[7]:

{2017: 'Rimini', 2018: 'Edinburgh', 2019: 'Basel', 2020: 'online'}

Или таким образом:

In [8]:

pycon.update(europython)
pycon

Out[8]:

{2017: 'Rimini', 2018: 'Edinburgh', 2019: 'Basel', 2020: 'online'}

Этот способ пойдойдет в случае если нужно изменить первоначальный словарь.

In [14]:

merged_dict = pycon.copy().update(europython)

Out[14]:

{2017: 'Portland', 2018: 'Cleveland', 2019: 'Cleveland', 2020: 'online'}

In [ ]:

Однако если нужно сохранить эти данные в новом словаре, этим способом воспользоваться нельзя, в последнем примере

In [17]:

merged_dict is None

Out[17]:

True

В библиотеке collections есть объект ChainMap, который тоже обхединяет словари, но его результатом будет объект ChainMap, а не словарь. Он состоит из нескольких словарей, следующих друг за другом. Если взять значение по ключу, то вернется то значение, которое встречается раньше. То есть, в этом примере для ключа 2019, вернется значение Cleveland

In [28]:

from collections import ChainMap
merged_dict = ChainMap(pycon, europython)
merged_dict

Out[28]:

ChainMap({2017: 'Amsterdam', 2018: 'Cleveland', 2019: 'Cleveland', 2020: 'online'}, {2017: 'Rimini', 2018: 'Edinburgh', 2019: 'Basel'})

В Python 3.8 был представлен walrus-оператор. С его помощью можно решить проблему примера с update и скопировать словарь в новую переменную:In [11]:

(merged_dict := pycon.copy()).update(europython)
merged_dict

Out[11]:

{2017: 'Rimini', 2018: 'Edinburgh', 2019: 'Basel', 2020: 'online'}

В Python 3.9 в PEP-0614 добавлен синтаксис | (читается как pipe), который объединяет словари аналогично первому примеру:

In [13]:

pycon | europython

Out[13]:

{2017: 'Rimini', 2018: 'Edinburgh', 2019: 'Basel', 2020: 'online'}

При этом первоначальный словарь не меняется:

In [14]:

pycon

Out[14]:

{2017: 'Portland', 2018: 'Cleveland', 2019: 'Cleveland', 2020: 'online'}

Если все-таки нужно обновить первоначальный словарь, можно воспользоваться синтаксисом |= (работает по аналогии с +=): a |= b a = a|bIn [16]:

pycon |= europython
pycon

Out[16]:

{2017: 'Rimini', 2018: 'Edinburgh', 2019: 'Basel', 2020: 'online'}

Важно помнить, что поскольку более поздние значения перезаписывают более ранние значения, эта операция может быть не коммуникативна.

In [23]:

print(europython | pycon)
print(pycon | europython)
{2017: 'Portland', 2018: 'Cleveland', 2019: 'Cleveland', 2020: 'online'}
{2017: 'Rimini', 2018: 'Edinburgh', 2019: 'Basel', 2020: 'online'}

Синтаксис | работает только непосредственно со словарями, тогда как синтаксис ** работает и с orderedict и с другими подобными словарям объектами.

Изменения в работе с декораторами

До версии 3.9 в качестве декораторов могли выступать функции или классы, но не такие объекты как списки или словари. Рассмотрим пример, в котором это будет удобно использовать. Допустим есть UI-приложение с кнопками, и нужно добавить какое-то сообщение по нажатию на каждую кнопку. Было бы удобно это сделать с помощью декоратора, но что делать, если для каждой кнопки нужно печатать различные сообщения. Это можно сделать следующим образом:In [ ]:

buttons = [QPushButton(f'Button {i}') for i in range(10)]
button_0 = buttons[0]
button_1 = buttons[1]

@button_0.clicked.connect
def say_hello():
    message.setText("Hello, World!")

@button_1.clicked.connect
def say_goodbye():
    message.setText("Goodbye, World!")

Создаем list comprehension из функций, явно присваем объектам значения элементов списка, и их можно использовать в качестве декораторов. Это будет работать, однако не будет эффективно, если объектов из которых будут созданы декораторы, будет достаточно много.

Это не единственный способ, но другие будут довольно неоднозначными. Рассмотрим два из них. В первом случае создадим функцию, которая будет возвращать необходимую для декоратора функцию:In [ ]:

def _(x):
    return x

@_(buttons[0].clicked.connect)
def say_hello():
    ...

Использование eval:In [ ]:

@eval("buttons[1].clicked.connect")
def say_bye():
    ...

В Python 3.9 стало возможно создавать декораторы из любых объектов, например из элементов списков и словарей. Можно рассмотреть синтаксис, который реализует тот же самый пример:

In [ ]:

@buttons[0].clicked.connect
def say_hello():
    message.setText("Hello, World!")

@buttons[1].clicked.connect
def say_goodbye():
    message.setText("Goodbye, World!")

Похожим образом будет выглядеть синтаксис для значений словаря:

In [ ]:

buttons = {'hello': QPushButton('Hello!'), 'goodbye': QPushButton('Goodbye!')}
@buttons['hello'].clicked.connect
def say_hello():
    message.setText("Hello, World!")

@buttons['goodbye'].clicked.connect
def say_goodbye():
    message.setText("Goodbye, World!")

Этот функционал, несмотря на то, что не приносит новых возможностей, позволяет писать более чистый код и избегать рискованных решений вроде использования eval.

Изменения в синтаксисе типирования

Дженерики

Дженерики это типы, которые могут быть параметризованы, обычно являются контейнерами, например dict. Параметризованные дженерики это типы, для которых указан внутренний тип, например dict[str, int]

Начиная с Python 3.7 стало можно указывать тип объекта, конкретизируя тип внутренних элементов контейнеры. Но для этого нужно было импортировать таки типы как List из модуля typing:

In [ ]:

from typing import List, Dict
def find(haystack: Dict[str, List[int]]) -> int:
#def find(haystack: dict) -> int:
    ...

Можно было не конкретизировать тип внутренних значений, для этого не нужно было импортировать дополинтельные типы и можно было использовать стандартные типы dict и list. Благодаря этому нововведению, внешние библиотеки такие как Mypy стали распознавать дженерики.

Также, в версии 3.9 стало можно пользоваться аннотациями без явного их импорта из модуля __future__

Расширение возможностей аннотаций

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

In [24]:

def speed(distance: "feet", time: "seconds") -> "miles per hour":
    fps2mph = 3600 / 5280 
    return distance / time * fps2mph

В Python 3.9 появился новый объект типа Annotated, который принимает на вход два аргумента, первый это реальный тип, которым должен обладать аннотриуемый объект, а второй это название для целей документации:

In [ ]:

from typing import Annotated

def speed(
    distance: Annotated[float, "feet"], time: Annotated[float, "seconds"]) -> Annotated[float, "miles per hour"]:
    fps2mph = 3600 / 5280  
    return distance / time * fps2mph

Когда происходит проверка аннотаций, проверяется только первый аргумент, в котором находится «реальный» тип объекта, а второй использовуется как и в предыдущих версиях:

In [25]:

speed.__annotations__

Out[25]:

{'distance': 'feet', 'time': 'seconds', 'return': 'miles per hour'}

Изменения связанные с часовыми поясами

В предыдущих версиях Python, модуль datetime не содержал информации о часовых поясах, и рекомендованным способом работы с ними была библиотека python-dateutil.

Однако начиная с Python 3.9 появился модуль zoneinfo, который содержит информацию о часовых поясах и позволяет создавать объекты им соответствующие:

In [31]:

from datetime import datetime, timezone
from zoneinfo import ZoneInfo
local_tz = ZoneInfo('Europe/Amsterdam')
datetime.now(tz=timezone.utc), datetime.now()

Out[31]:

(datetime.datetime(2021, 5, 4, 15, 58, 36, 295472, tzinfo=datetime.timezone.utc),
 datetime.datetime(2021, 5, 4, 17, 58, 36, 295477))

В стандартной библиотеке не было информации ни о каких часовых поясах, кроме utc, и необходимо было использовать pytz чтобы создать объект, содержащий информацию о часовом поясе. Теперь это можно сделать без установки дополнительных библиотек:In [32]:

from zoneinfo import ZoneInfo
local_tz = ZoneInfo('Europe/Amsterdam')
datetime.now(tz=local_tz)

Out[32]:

(datetime.datetime(2021, 5, 4, 17, 58, 37, 765947, tzinfo=zoneinfo.ZoneInfo(key='Europe/Amsterdam')),)

In [ ]:

Всего часовых поясов больше чем можно было бы предположить, и многие из них представлены в модуле `zoneinfo`

In [33]:

import zoneinfo
tzs = zoneinfo.available_timezones()
len(tzs)

Out[33]:

594

Новые методы работы со строкам

Иногда нужно убрать первые или последние несколько символов из строки. Есть метод strip, который обладает казалось бы похожим функционалом, но он может иногда давать неожиданные результаты Например в таком случае:

In [34]:

"ababbbbbbaaccc".lstrip("ab")

Out[34]:

'ccc'

В Python 3.9 были добавлены функции removeprefixremovesuffix, которые выполняют то, что от них ожидается: удаляют первые или последние несколько символов строки.In [35]:

"ababbbbbbaaccc".removeprefix("ab")

Out[35]:

'abbbbbbaaccc'

In [36]:

"ababbbbbbaaccc".removesuffix("c")

Out[36]:

'ababbbbbbaacc'

In [36]:

Если строка не содержит суффикс или префикс, строка не меняется, никаких исключений не появляется.

In [37]:

"ababbbbbbaaccc".removesuffix("something else")

Out[37]:

'ababbbbbbaaccc'

Ссылки:

Python 3.10

Последняя версия Python 3.10 это 0b3, которая вышла 17 июня 2021 года. Согласно расписанию релизов, полноценный релиз появится примерно в октябре 2021. Пока официального релиза нет, версия не доступна в пакетных менеджерах (таких как brew). Соответственно, чтобы ее установить, билд нужно скачать вручную с www.python.org и уствноваить на своей машине. Рассмотрим изменения, доступные в этой Python 3.9.

Pattern Matching

В более ранних версиях Python если нужно описать различное поведение, в зависимости от того, какой объект используется, скорее всего будет использоваться условный оператор:In [ ]:

if isinstance(x, tuple) and len(x) == 2:
    host, port = x
    mode = "http"
elif isinstance(x, tuple) and len(x) == 3:
    host, port, mode = x

Однако в Python 3.10 вместе с PEP-0635 будет добавлен новый оператор match, который может использоваться с case:In [ ]:

match x:
    case host, port:
        mode = "http"
    case host, port, mode:
        pass
    cass Class1:
        ...
    case host == 'localhost'
    ...

Также с его помощью будет удобно реализовывать, например, обработку ошибок:In [ ]:

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the Internet"

Это одно из самых «громких» нововведений в последних версиях Python и возможно самое ожидаемое коммьюнити.

Более удобное использование Union для типирования:

Если раньше чтобы указать, что аргумент функции может быть одним из нескольких типов, необходимо было использовать Union, импортируемый из пакета typing:In [ ]:

from typing import Union

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

То начиная с Python 3.10 можно будет заменить его оператором pipe | и, соответственно, избавиться от импорта:In [ ]:

def square(number: int | float) -> int | float:
    return number ** 2

Контекстные менеджеры

Синтаксис работы с контекстными менеджерами упростится, теперь несколько контекстных менеджеров можно объединять в одном with, а так же не обязательно присваивать им локальные имена:In [ ]:

with (
    CtxManager1(),
    CtxManager2()
):
    ...

with (CtxManager1() as example,
      CtxManager2()):
    ...

with (
    CtxManager1() as example1,
    CtxManager2() as example2
):
    ...

Более информативные сообщения об ошибках

Наверняка многие из читателей сталкивались с тем, что сообщение об ошибке слишком общее и само по себе не говорит о том, что нужно сделать чтобы ошибку исправить. В новой версиеи Python core-разработчики улучшили самые распространенные из них и теперь сообщения об ошибках будут намного более читабельными:In [ ]:

expected = {9: 1, 18: 2, 19: 2, 27: 3, 28: 3, 29: 3, 36: 4, 37: 4,
           ^
SyntaxError: '{' was never closed

In [ ]:

>>> if rocket.position > event_horizon
  File "<stdin>", line 1
    if rocket.position > event_horizon
                                      ^
SyntaxError: expected ':'

In [ ]:

>>> {x,y for x,y in range(100)}
  File "<stdin>", line 1
    {x,y for x,y in range(100)}
     ^
SyntaxError: did you forget parentheses around the comprehension target?

In [ ]:

if rocket.position = event_horizon:
  File "<stdin>", line 1
    if rocket.position = event_horizon:
                       ^
SyntaxError: cannot assign to attribute here. Maybe you meant '==' instead

In [ ]:

def foo():
...    if lel:
...    x = 2
  File "<stdin>", line 3
    x = 2
    ^
IndentationError: expected an indented block after 'if' statement in line 2

Остальное

  • Модуль distutils постепенно выыводится из употребления, чтобы быть полностью удаленным в Python 3.12. Это связано с тем, что его функции были полностью заменены такими модулями как setuptools и packaging.
  • Улучшение читабельности сообщений в модуле debugging

Это не все изменения которые попадут в Python 3.10, с полным списком можно ознакомиться посмотрев список изменений.


Прокачать Python можно на курсах: