Новые интересные функции в Python 3.8

Spread the love

В статье описывается улучшения и новый функционал добавленные в недавно выпущенную очередную версию Python 3.8.

Оригинальная версия статьи: Geir Arne HjelleCool New Features in Python 3.8

Содержание

Выпущена новейшая версия Python! Python 3.8 был доступен в бета-версиях с лета 2019, а 14 октября 2019 года вышла первая официальная версия. Теперь мы все сможем поиграть с новым функционалом и воспользоваться последними улучшениями.

Что нового в Python 3.8? В официальной документация приведен хороший обзор новых функций. Тем не менее, эта статья более подробно расскажет о некоторых самых значительных изменениях и покажет, как вы сможете воспользоваться преимуществами Python 3.8.

В этой статье вы узнаете о:

  • Использование выражений присваивания для упрощения некоторых конструкций кода
  • Применение только позиционных аргументов в ваших собственных функциях
  • Задания более точных подсказок типов
  • Использование f-строк для более простой отладки

За некоторыми исключениями, Python 3.8 содержит множество небольших улучшений по сравнению с более ранними версиями. В конце статьи вы увидите многие из этих менее привлекающих внимание изменений, а также мы рассмотрим некоторые оптимизации, которые Python 3.8 делает быстрее, чем его предшественники. Наконец, вы получите несколько советов по обновлению до новой версии.

Морж в комнате: выражение присваивания

Самое большое изменение в Python 3.8 — это введение выражений присваивания. Они написаны с использованием новой записи (: =). Этого оператора часто называют оператор морж (walrus), так как он напоминает глаза и бивни моржа на боку.

Выражения присваивания позволяют вам присваивать и возвращать значение в одном выражении. Например, если вы хотите присвоить переменную и вывести ее значение, вы обычно делаете что-то вроде этого:

>>> walrus = False
>>> print(walrus)
False

В Python 3.8 вам разрешено объединять эти два оператора в один, используя оператор walrus:

>>> print(walrus := True)
True

Выражение присваивания позволяет присвоить True walrus и сразу же вывести значение. Но имейте в виду, что оператор walrus не делает ничего такого чего нельзя было бы сделать без него. Он только делает некоторые конструкции более удобными, и иногда может более четко сообщать о намерениях вашего кода.

Один из примеров, демонстрирующий некоторые сильные стороны оператора walrus, — это циклы while, где вам нужно инициализировать и обновить переменную. Например, следующий код запрашивает ввод у пользователя, пока он не наберет quit:

inputs = list()
current = input("Write something: ")
while current != "quit":
    inputs.append(current)
    current = input("Write something: ")

Этот код не идеален. Вы повторяете оператор input() два раза, так как вам нужно каким-то образом получить current, прежде чем зайти в цикл. Лучшее решение — установить бесконечный цикл while и использовать break для остановки цикла:

inputs = list()
while True:
    current = input("Write something: ")
    if current == "quit":
        break
    inputs.append(current)

Этот код эквивалентен приведенному выше, но избегает повторения и каким-то образом сохраняет строки в более логичном порядке. Если вы используете выражение присваивания, вы можете еще больше упростить этот цикл:

inputs = list()
while (current := input("Write something: ")) != "quit":
    inputs.append(current)

PEP 572 описывает более подробно выражения присваивания, включая некоторые обоснования для их введения в язык, а также дает несколько примеров того, как можно использовать оператор walrus.

Только позиционные аргументы

Встроенная функция float() может использоваться для преобразования текстовых строк и чисел в float объекты. Рассмотрим следующий пример:

>>> float("3.8")
3.8

>>> help(float)
class float(object)
 |  float(x=0, /)
 |  
 |  Convert a string or number to a floating point number, if possible.

[...]

Посмотрите внимательно на float(). Обратите внимание на косую черту (/) после параметра. Что это значит?

Примечание. Для более подробного обсуждения нотации / см. PEP 457 — Нотация только позиционных параметров.

Оказывается, что хотя один параметр float() называется x, вы не можете использовать его имя:

>>> float(x="3.8")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: float() takes no keyword arguments

При использовании float() вам разрешено указывать аргументы только по позиции, а не по ключевому слову. До Python 3.8 такие позиционные аргументы были возможны только для встроенных функций. Не было простого способа указать, что аргументы должны быть только позиционными в ваших собственных функциях:

>>> def incr(x):
...     return x + 1
... 
>>> incr(3.8)
4.8

>>> incr(x=3.8)
4.8

Можно было имитировать только позиционные аргументы, используя *args, но это менее гибко, менее читабельно и заставляет вас реализовать собственный анализ аргументов. В Python 3.8 вы можете использовать /, чтобы обозначить, что все аргументы перед ним должны быть указаны только позицией. Вы можете переписать incr(), чтобы принимать только позиционные аргументы:

>>> def incr(x, /):
...     return x + 1
... 
>>> incr(3.8)
4.8

>>> incr(x=3.8)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: incr() got some positional-only arguments passed as
           keyword arguments: 'x'

Добавляя / после x, вы указываете, что x является позиционным аргументом. Вы можете комбинировать обычные аргументы с позиционными только, поместив обычные аргументы после косой черты:

>>> def greet(name, /, greeting="Hello"):
...     return f"{greeting}, {name}"
... 
>>> greet("Łukasz")
'Hello, Łukasz'

>>> greet("Łukasz", greeting="Awesome job")
'Awesome job, Łukasz'

>>> greet(name="Łukasz", greeting="Awesome job")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: greet() got some positional-only arguments passed as
           keyword arguments: 'name'

В greet() косая черта помещается между name и greeting. Это означает, что name является позиционным аргументом, а greeting является обычным аргументом, который может передаваться либо по позиции, либо по ключевому слову.

На первый взгляд, позиционные аргументы могут показаться немного ограниченными и противоречащими мантре Python о важности читабельности. Вы, вероятно, обнаружите, что не так много случаев, когда позиционные аргументы улучшают ваш код.

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

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

Позиционные аргументы хорошо дополняют аргументы только с ключевыми словами. В любой версии Python 3 вы можете указать аргументы только для ключевых слов, используя звездочку (*). Любой аргумент после * должен быть указан с помощью ключевого слова:

>>> def to_fahrenheit(*, celsius):
...     return 32 + celsius * 9 / 5
... 
>>> to_fahrenheit(40)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: to_fahrenheit() takes 0 positional arguments but 1 was given

>>> to_fahrenheit(celsius=40)
104.0

celsius является аргументом только для ключевых слов, поэтому Python выдает ошибку, если вы пытаетесь указать его на основе позиции, без ключевого слова.

Вы можете комбинировать только позиционные, обычные и ключевые слова, указав их в этом порядке через / и *. В следующем примере text является позиционным аргументом, border является обычным аргументом со значением по умолчанию, а width является аргументом только для ключевого слова со значением по умолчанию:

>>> def headline(text, /, border="♦", *, width=50):
...     return f" {text} ".center(width, border)
... 

Поскольку text только позиционный, то вы не можете использовать ключевое слово text:

>>> headline("Positional-only Arguments")
'♦♦♦♦♦♦♦♦♦♦♦ Positional-only Arguments ♦♦♦♦♦♦♦♦♦♦♦♦'

>>> headline(text="This doesn't work!")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: headline() got some positional-only arguments passed as
           keyword arguments: 'text'

border, с другой стороны, может быть указана как с ключевым словом, так и без него:

>>> headline("Python 3.8", "=")
'=================== Python 3.8 ==================='

>>> headline("Real Python", border=":")
':::::::::::::::::: Real Python :::::::::::::::::::'

Наконец, width должна быть указана только с помощью ключевого слова:

>>> headline("Python", "🐍", width=38)
'🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍 Python 🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍'

>>> headline("Python", "🐍", 38)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: headline() takes from 1 to 2 positional arguments
           but 3 were given

Вы можете прочитать больше о позиционных аргументах в PEP 570.

Более точные подсказки для типов

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

  • тип Literal
  • Типизированные словари
  • объекты Final
  • Protocols

Python поддерживает необязательные подсказки типов, обычно в виде аннотаций в вашем коде:

def double(number: float) -> float:
    return 2 * number

В этом примере вы говорите, что number должно быть числом с плавающей точкой, и функция double() также должна возвращать float. Тем не менее, Python рассматривает эти аннотации как подсказки. Они не применяются во время выполнения:

>>> double(3.14)
6.28

>>> double("I'm not a float")
"I'm not a floatI'm not a float"

double() с радостью принимает в качестве аргумента слово «I’m not a float», хотя это и не число с плавающей точкой. Существуют библиотеки, которые могут использовать типы во время выполнения, но это не основной вариант использования системы типов Python.

Вместо этого подсказки типов позволяют статическим средствам проверки типов выполнять проверку типов вашего кода Python без фактического запуска ваших сценариев. Это напоминает компиляторы, улавливающие ошибки типов в других языках, таких как Java, Rust и Crystal. Кроме того, подсказки типа действуют как документация вашего кода, облегчая чтение, а также улучшая автозаполнение в вашей IDE.

Примечание. Доступно несколько средств проверки статического типа, в том числе Pyright, Pytype и Pyre. В этой статье вы будете использовать Mypy. Вы можете установить Mypy из PyPI, используя pip:

$ python -m pip install mypy

В некотором смысле Mypy является эталонной реализацией средства проверки типов для Python и разрабатывается в Dropbox под руководством Юкки Лехтасало. Так же создатель Python, Гвидо ван Россум, является частью команды Mypy.

Вы можете найти больше информации о подсказках типов в Python в исходном PEP 484, а также в Python Type Check (Руководство).

Было создано четыре новых PEP о проверке типов, которые были приняты и включены в Python 3.8. Вы рассмотрим короткие примеры для каждого из них.

PEP 586 вводит тип Literal. Literal немного особенный в том смысле, что он представляет одно или несколько конкретных значений. Одним из вариантов использования Literal является возможность точного добавления типов, когда строковые аргументы используются для описания конкретного поведения. Рассмотрим следующий пример:

# draw_line.py

def draw_line(direction: str) -> None:
    if direction == "horizontal":
        ...  # Draw horizontal line

    elif direction == "vertical":
        ...  # Draw vertical line

    else:
        raise ValueError(f"invalid direction {direction!r}")

draw_line("up")

Программа пройдет проверку статического типа, даже если «up» является недопустимым направлением. Средство проверки типов только проверяет, что «up» является строкой. В этом случае было бы точнее сказать, что направление должно быть либо литеральной строкой «horizontal», либо литеральной строкой «vertical». Используя Literal, вы можете сделать именно это:

# draw_line.py

from typing import Literal

def draw_line(direction: Literal["horizontal", "vertical"]) -> None:
    if direction == "horizontal":
        ...  # Draw horizontal line

    elif direction == "vertical":
        ...  # Draw vertical line

    else:
        raise ValueError(f"invalid direction {direction!r}")

draw_line("up")

Выставив допустимые значения direction для средства проверки типов, теперь вы будете предупреждены об ошибке:

$ mypy draw_line.py 
draw_line.py:15: error:
    Argument 1 to "draw_line" has incompatible type "Literal['up']";
    expected "Union[Literal['horizontal'], Literal['vertical']]"
Found 1 error in 1 file (checked 1 source file)

Основной синтаксис Literal[<literal>]. Например, Literal [38] представляет буквальное значение 38. Вы можете выразить одно из нескольких литеральных значений, используя Union:

Union[Literal["horizontal"], Literal["vertical"]]

Поскольку это довольно распространенный вариант использования, вы можете (и, вероятно, должны) использовать вместо него более простую нотацию Literal[«horizontal», «vertical»]. Если вы внимательно посмотрите на вывод Mypy, приведенный выше, то увидите, что он перевел простую запись в нотацию Union внутри.

Есть случаи, когда тип возвращаемого значения функции зависит от входных аргументов. Одним из примеров является open(), который может возвращать текстовую строку или байтовый массив в зависимости от значения mode. Это может быть решено путем перегрузки.

В следующем примере показан скелет калькулятора, который может возвращать ответ либо в виде обычных чисел (38), либо в виде римских цифр (XXXVIII):

# calculator.py

from typing import Union

ARABIC_TO_ROMAN = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
                   (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
                   (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]

def _convert_to_roman_numeral(number: int) -> str:
    """Convert number to a roman numeral string"""
    result = list()
    for arabic, roman in ARABIC_TO_ROMAN:
        count, number = divmod(number, arabic)
        result.append(roman * count)
    return "".join(result)

def add(num_1: int, num_2: int, to_roman: bool = True) -> Union[str, int]:
    """Add two numbers"""
    result = num_1 + num_2

    if to_roman:
        return _convert_to_roman_numeral(result)
    else:
        return result

Код имеет правильные подсказки типа: результат add() будет либо str, либо int. Однако часто этот код вызывается с литералом True или False в качестве значения to_roman, и в этом случае вы хотите, чтобы средство проверки типов точно определило, возвращается или str или int. Это можно сделать, используя Literal вместе с @overload:

# calculator.py

from typing import Literal, overload, Union

ARABIC_TO_ROMAN = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
                   (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
                   (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]

def _convert_to_roman_numeral(number: int) -> str:
    """Convert number to a roman numeral string"""
    result = list()
    for arabic, roman in ARABIC_TO_ROMAN:
        count, number = divmod(number, arabic)
        result.append(roman * count)
    return "".join(result)

@overload
def add(num_1: int, num_2: int, to_roman: Literal[True]) -> str: ...
@overload
def add(num_1: int, num_2: int, to_roman: Literal[False]) -> int: ...

def add(num_1: int, num_2: int, to_roman: bool = True) -> Union[str, int]:
    """Add two numbers"""
    result = num_1 + num_2

    if to_roman:
        return _convert_to_roman_numeral(result)
    else:
        return result

Добавленные сигнатуры @overload помогут вашему контролеру типов выводить str или int в зависимости от литеральных значений to_roman. Обратите внимание, что (…) являются буквальной частью кода. Они заменяют тело функции в перегруженных сигнатурах.

В дополнение к Literal, PEP 591 представляет Final. Этот квалификатор указывает, что переменная или атрибут не должны быть переназначены или переопределены. Ниже приведена ошибка ввода:

from typing import Final

ID: Final = 1

...

ID += 1

Mypy выделит строку ID += 1 и выведет сообщение: Cannot assign to final name «ID». Это дает вам возможность гарантировать, что константы в вашем коде никогда не изменят свое значение.

Кроме того, декоратор @final, можно применять к классам и методам. Классы, декорируемые @final, не могут быть разделены на подклассы, в то время как методы @final не могут быть переопределены подклассами:

from typing import final

@final
class Base:
    ...

class Sub(Base):
    ...

Mypy пометит этот пример сообщением об ошибке Cannot inherit from final class «Base». Чтобы узнать больше о Final и @final, см. PEP 591.

Третий PEP, допускающий более конкретные подсказки типов, — это PEP 589, который вводит TypedDict. Это можно использовать для указания типов для ключей и значений в словаре с использованием нотации, аналогичной типизированному NamedTuple.

Традиционно, словари были аннотированы с помощью Dict. Проблема заключается в том, что это разрешает только один тип для ключей и один тип для значений, что часто приводит к аннотациям, таким как Dict[str, Any]. В качестве примера рассмотрим словарь, который регистрирует информацию о версиях Python:

py38 = {"version": "3.8", "release_year": 2019}

Значение, соответствующее version, является строкой, а release_year является целым числом. Это не может быть точно представлено с помощью Dict. С новым TypedDict вы можете сделать следующее:

from typing import TypedDict

class PythonVersion(TypedDict):
    version: str
    release_year: int

py38 = PythonVersion(version="3.8", release_year=2019)

Затем средство проверки типов сможет сделать вывод, что py38 [«version»] имеет тип str, а py38 [«release_year»] является int. Во время выполнения TypedDict является обычным dict, а подсказки типов игнорируются как обычно. Вы также можете использовать TypedDict исключительно в качестве аннотации:

py38: PythonVersion = {"version": "3.8", "release_year": 2019}

Mypy сообщит вам, если какое-либо из ваших значений имеет неправильный тип, или если вы используете ключ, который не был объявлен. См. PEP 589 для большего количества примеров.

Mypy уже некоторое время поддерживает Protocols. Однако официальное принятие произошло только в мае 2019 года.

Протоколы — это способ формализовать поддержку Python утиного типа данных:

Когда я вижу птицу, которая ходит, как утка, и плавает, как утка, и крякает, как утка, я называю эту птицу уткой. (Источник)

Утиный тип позволяет, например, читать .name для любого объекта, имеющего атрибут .name, не заботясь о типе объекта. Это может показаться нелогичным для системы типов, чтобы поддержать это.

Например, вы можете определить протокол с именем Named, который может идентифицировать все объекты с атрибутом .name:

from typing import Protocol

class Named(Protocol):
    name: str

def greet(obj: Named) -> None:
    print(f"Hi {obj.name}")

Здесь greet() принимает любой объект, если он определяет атрибут .name. См. PEP 544 и документацию Mypy для получения дополнительной информации о протоколах.

Более простая отладка с помощью f-строк

f-строки были введены в Python 3.6 и стали очень популярными. Они могут быть самой распространенной причиной поддержки библиотек Python только в версии 3.6 и более поздних. F-строка — это форматированный строковый литерал. Вы можете узнать его по ведущему символу f:

>>> style = "formatted"
>>> f"This is a {style} string"
'This is a formatted string'

Когда вы используете f-строки, вы можете заключать переменные и даже выражения в фигурные скобки. Затем они будут оценены во время выполнения и включены в строку. Вы можете иметь несколько выражений в одной f-строке:

>>> import math
>>> r = 3.6

>>> f"A circle with radius {r} has area {math.pi * r * r:.2f}"
'A circle with radius 3.6 has area 40.72'

В последнем выражении {math.pi * r * r: .2f} вы также используете спецификатор формата. Спецификаторы формата отделяются от выражений двоеточием.

.2f означает, что область отформатирована как число с плавающей запятой с двумя десятичными знаками. Спецификаторы формата такие же, как и для .format (). См. официальную документацию для полного списка разрешенных спецификаторов формата.

В Python 3.8 вы можете использовать выражения присваивания внутри f-строк. Просто убедитесь, что выражение присваивания заключено в круглые скобки:

>>> import math
>>> r = 3.8

>>> f"Diameter {(diam := 2 * r)} gives circumference {math.pi * diam:.2f}"
'Diameter 7.6 gives circumference 23.88'

Однако настоящая новость в Python 3.8 — это новый спецификатор отладки. Теперь вы можете добавить = в конце выражения, и оно напечатает как выражение, так и его значение:

>>> python = 3.8
>>> f"{python=}"
'python=3.8'

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

>>> python = 3.7
>>> f"python={python}"
'python=3.7'

Вы можете добавить пробелы вокруг = и использовать спецификаторы формата как обычно:

>>> name = "Eric"
>>> f"{name = }"
"name = 'Eric'"

>>> f"{name = :>10}"
'name =       Eric'

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

>>> f"{name.upper()[::-1] = }"
"name.upper()[::-1] = 'CIRE'"

Для получения дополнительной информации о f-строках см. Python 3’s f-Strings: An Improved String Formatting Syntax (Guide).

Руководящий совет Python

Технически руководство Python не должно заниматься разработкой языка. Тем не менее, Python 3.8 является первой версией Python, разработку которой не вел Гвидо ван Россума. Язык Python теперь регулируется руководящим советом, состоящим из пяти основных разработчиков:

Дорога к новой модели управления для Python была интересным исследованием самоорганизации. Гвидо ван Россум (Guido van Rossum) создал Python в начале 1990-х годов и с любовью получил звание «Пожизненный доброжелательный диктатор» (BDFL Benevolent Dictator for Life). На протяжении многих лет все больше и больше решений о языке Python принималось с помощью предложений по улучшению Python (PEP). Тем не менее, у Гвидо официально было последнее слово в любой новой языковой функции.

После долгой и продолжительной дискуссии о выражениях присваивания, Гвидо объявил в июле 2018 года, что уходит в отставку с поста BDFL (на этот раз по-настоящему). Он целенаправленно не назвал преемника. Вместо этого он попросил команду разработчиков ядра выяснить, как Python должен управляться в будущем.

К счастью, процесс PEP уже хорошо отработан, поэтому было естественно использовать PEP для обсуждения и принятия решения о новой модели управления. Осенью 2018 года было предложено несколько моделей, в том числе избрание нового BDFL (переименованного в Gracious Umpire, ответственного за принятие решений: GUIDO) или переход к модели сообщества, основанной на консенсусе и голосовании, без централизованного руководства. В декабре 2018 года модель руководящего совета была выбрана после голосования среди основных разработчиков.

The Python Steering Council at PyCon 2019
Руководящий совет Python на PyCon 2019. Слева направо: Барри Варшава, Бретт Кэннон, Кэрол Виллинг, Гвидо ван Россум и Ник Коглан (Изображение: Geir Arne Hjelle)

Руководящий совет состоит из пяти членов сообщества Python, как указано выше. После каждого основного выпуска Python будут выборы нового руководящего совета. Другими словами, после выпуска Python 3.8 пройдут выборы.

Хотя это и открытые выборы, ожидается, что большинство, если не все, инаугурационного руководящего совета будут переизбраны. Руководящий совет обладает широкими полномочиями принимать решения о языке Python, но должен стремиться использовать эти полномочия как можно меньше.

Вы можете прочитать все о новой модели управления в PEP 13, в то время как процесс принятия решения о новой модели описан в PEP 8000. Для получения дополнительной информации см. PyCon 2019 Keynote и прослушайте Brett Cannon на Talk Python To Me  и на The Changelog podcast. Вы можете следить за обновлениями в Руководящем совете через GitHub.

Другие довольно интересные функции

До сих пор вы видели заголовки новостей о том, что нового в Python 3.8. Тем не менее, есть много других изменений, которые тоже довольно крутые. В этом разделе вы познакомитесь с некоторыми из них.

importlib.metadata

В стандартной библиотеке Python 3.8 доступен один новый модуль: importlib.metadata. С помощью этого модуля вы можете получить доступ к информации об установленных пакетах в вашей установке Python. Вместе с сопутствующим модулем importlib.resources, importlib.metadata улучшает функциональность более старых pkg_resources.

Например, вы можете получить информацию о pip:

>>> from importlib import metadata
>>> metadata.version("pip")
'19.2.3'

>>> pip_metadata = metadata.metadata("pip")
>>> list(pip_metadata)
['Metadata-Version', 'Name', 'Version', 'Summary', 'Home-page', 'Author',
 'Author-email', 'License', 'Keywords', 'Platform', 'Classifier',
  'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier',
  'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier',
  'Classifier', 'Classifier', 'Requires-Python']

>>> pip_metadata["Home-page"]
'https://pip.pypa.io/'

>>> pip_metadata["Requires-Python"]
'>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*'

>>> len(metadata.files("pip"))
668

На данный момент установлена версия pip 19.2.3. metadata() предоставляет доступ к большей части информации, которую вы можете увидеть в PyPI. Например, вы можете видеть, что для этой версии pip требуется либо Python 2.7, либо Python 3.5 или выше. С помощью files() вы получите список всех файлов, которые составляют пакет pip. В этом случае существует почти 700 файлов.

files() возвращает список объектов Path. Это дает вам удобный способ просмотра исходного кода пакета, используя read_text(). В следующем примере выводится __init__.py из пакета realpython-reader:

>>> [p for p in metadata.files("realpython-reader") if p.suffix == ".py"]
[PackagePath('reader/__init__.py'), PackagePath('reader/__main__.py'),
 PackagePath('reader/feed.py'), PackagePath('reader/viewer.py')]

>>> init_path = _[0]  # Underscore access last returned value in the REPL
>>> print(init_path.read_text())
"""Real Python feed reader

Import the `feed` module to work with the Real Python feed:

    >>> from reader import feed
    >>> feed.get_titles()
    ['Logging in Python', 'The Best Python Books', ...]

See https://github.com/realpython/reader/ for more information
"""

# Version of realpython-reader package
__version__ = "1.0.0"

...

Вы также можете получить доступ к зависимостям пакетов:

>>> metadata.requires("realpython-reader")
['feedparser', 'html2text', 'importlib-resources', 'typing']

requires() перечисляет зависимости пакета. Вы можете видеть, что realpython-reader, например, использует feedparser в фоновом режиме для чтения и анализа потока статей.

В PyPI имеется резервный бэкпорт importlib.metadata, который работает в более ранних версиях Python. Вы можете установить его используя pip:

$ python -m pip install importlib-metadata

Вы можете использовать бэкпорт PyPI в своем коде следующим образом:

try:
    from importlib import metadata
except ImportError:
    import importlib_metadata as metadata

...

См. документацию для получения дополнительной информации о importlib.metadata.

Новые и улучшенные функции math и statistics

Python 3.8 вносит множество улучшений в существующие стандартные библиотечные пакеты и модули. math в стандартной библиотеке имеет несколько новых функций. math.prod() работает аналогично встроенной функции sum(), но для мультипликативных продуктов:

>>> import math
>>> math.prod((2, 8, 7, 7))
784

>>> 2 * 8 * 7 * 7
784

Два утверждения эквивалентны. prod() будет проще использовать, когда у вас уже есть множители, сохраненные в итерируемом объекте.

Еще одна новая функция — math.isqrt(). Вы можете использовать isqrt(), чтобы найти целую часть квадратных корней:

>>> import math
>>> math.isqrt(9)
3

>>> math.sqrt(9)
3.0

>>> math.isqrt(15)
3

>>> math.sqrt(15)
3.872983346207417

Квадратный корень из 9 равен 3. Вы можете видеть, что isqrt() возвращает целочисленный результат, тогда как math.sqrt() всегда возвращает float. Квадратный корень из 15 почти 3,9. Обратите внимание, что isqrt() усекает ответ до следующего целого числа, в данном случае 3.

Наконец, теперь вы можете проще работать с n-мерными точками и векторами в стандартной библиотеке. Вы можете найти расстояние между двумя точками с помощью math.dist() и длину вектора с помощью math.hypot():

>>> import math
>>> point_1 = (16, 25, 20)
>>> point_2 = (8, 15, 14)

>>> math.dist(point_1, point_2)
14.142135623730951

>>> math.hypot(*point_1)
35.79106033634656

>>> math.hypot(*point_2)
22.02271554554524

Это облегчает работу с точками и векторами с использованием стандартной библиотеки. Однако, если вы будете делать много вычислений для точек или векторов, вам лучше работать с NumPy.

Модуль statistics также имеет несколько новых функций:

  • statistics.fmean() вычисляет среднее число float.
  • statistics.geometric_mean()вычисляет среднее геометрическое число float.
  • statistics.multimode() находит наиболее часто встречающиеся значения в последовательности.
  • statistics.quantiles() вычисляет точки разреза для разделения данных на n непрерывных интервалов с равной вероятностью.

В следующем примере показаны используемые функции:

>>> import statistics
>>> data = [9, 3, 2, 1, 1, 2, 7, 9]
>>> statistics.fmean(data)
4.25

>>> statistics.geometric_mean(data)
3.013668912157617

>>> statistics.multimode(data)
[9, 2, 1]

>>> statistics.quantiles(data, n=4)
[1.25, 2.5, 8.5]

В Python 3.8 появился новый класс statistics.NormalDist, который делает его более удобным для работы с нормальным гауссовым распределением.

Чтобы увидеть пример использования NormalDist, вы можете попробовать сравнить скорость новой statistics.fmean() и традиционной statistics.mean():

>>> import random
>>> import statistics
>>> from timeit import timeit

>>> # Create 10,000 random numbers
>>> data = [random.random() for _ in range(10_000)]

>>> # Measure the time it takes to run mean() and fmean()
>>> t_mean = [timeit("statistics.mean(data)", number=100, globals=globals())
...           for _ in range(30)]
>>> t_fmean = [timeit("statistics.fmean(data)", number=100, globals=globals())
...            for _ in range(30)]

>>> # Create NormalDist objects based on the sampled timings
>>> n_mean = statistics.NormalDist.from_samples(t_mean)
>>> n_fmean = statistics.NormalDist.from_samples(t_fmean)

>>> # Look at sample mean and standard deviation
>>> n_mean.mean, n_mean.stdev
(0.825690647733245, 0.07788573997674526)

>>> n_fmean.mean, n_fmean.stdev
(0.010488564966666065, 0.0008572332785645231)

>>> # Calculate the lower 1 percentile of mean
>>> n_mean.quantiles(n=100)[0]
0.6445013221202459

В этом примере мы используем timeit для измерения времени выполнения mean() и fmean(). Чтобы получить надежные результаты, мы позволяем timeit выполнять каждую функцию 100 раз и собираем 30 таких временных выборок для каждой функции. На основе этих примеров мы создаем два объекта NormalDist. Обратите внимание: если вы запускаете код самостоятельно, сбор различных временных примеров может занять до минуты.

NormalDist имеет много удобных атрибутов и методов. Смотрите документацию для полного списка. Изучив .mean и .stdev, вы увидите, что старый statistics.mean() выполняется за 0,826 ± 0,078 секунды, а новый statistics.fmean() тратит 0,0105 ± 0,0009 секунды. Другими словами, fmean() примерно в 80 раз быстрее для этих данных.

Если вам нужна более сложная статистика в Python, чем предлагает стандартная библиотека, посмотрите statsmodels и scipy.stats.

Предупреждения об опасном синтаксисе

В Python есть SyntaxWarning, которое может предупреждать о сомнительном синтаксисе, который обычно не является ошибкой синтаксиса SyntaxError. Python 3.8 добавляет несколько новых, которые могут помочь вам во время кодирования и отладки.

Разница между is и == может сбивать с толку. Последний проверяет наличие одинаковых значений, в то время как значение True является истинным, только если объекты совпадают. Python 3.8 попытается предупредить вас о случаях, когда вы должны использовать == вместо is:

>>> # Python 3.7
>>> version = "3.7"
>>> version is "3.7"
False

>>> # Python 3.8
>>> version = "3.8"
>>> version is "3.8"
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
False

>>> version == "3.8"
True

Запятую легко пропустить, когда вы пишете длинный список, особенно при его вертикальном форматировании. Если забыть запятую в списке кортежей, появится сообщение об ошибке о том, что кортежи не могут быть вызваны. Python 3.8 дополнительно выдает предупреждение, указывающее на реальную проблему:

>>> [
...   (1, 3)
...   (2, 4)
... ]
<stdin>:2: SyntaxWarning: 'tuple' object is not callable; perhaps
           you missed a comma?
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: 'tuple' object is not callable

Предупреждение правильно идентифицирует пропущенную запятую как настоящего преступника.

Оптимизации

Для Python 3.8 сделано несколько оптимизаций. Некоторые заставляют код работать быстрее, другие уменьшают используемый объем памяти. Например, поиск полей в namedtuple значительно быстрее в Python 3.8 по сравнению с Python 3.7:

>>> import collections
>>> from timeit import timeit
>>> Person = collections.namedtuple("Person", "name twitter")
>>> raymond = Person("Raymond", "@raymondh")

>>> # Python 3.7
>>> timeit("raymond.twitter", globals=globals())
0.05876131607996285

>>> # Python 3.8
>>> timeit("raymond.twitter", globals=globals())
0.0377705999400132

Вы можете видеть, что поиск .twitter в namedtuple на 30-40% быстрее в Python 3.8. Списки сохраняют некоторое пространство, когда они инициализируются из итераций с известной длиной. Это может сэкономить память:

>>> import sys

>>> # Python 3.7
>>> sys.getsizeof(list(range(20191014)))
181719232

>>> # Python 3.8
>>> sys.getsizeof(list(range(20191014)))
161528168

В этом случае список использует примерно на 11% меньше памяти в Python 3.8 по сравнению с Python 3.7.

Другие оптимизации включают более высокую производительность в subprocess, более быстрое копирование файлов с помощью shutil, улучшенную производительность по умолчанию для pickle и более быстрые операции operator.itemgetter. Смотрите официальную документацию для полного списка оптимизаций.

Итак, стоит ли переходить на Python 3.8?

Давайте начнем с простого ответа. Если вы хотите опробовать какие-либо новые функции, которые вы видели здесь, вам нужно иметь возможность использовать Python 3.8. Такие инструменты, как pyenv и Anaconda, позволяют легко устанавливать несколько версий Python одновременно. Кроме того, вы можете запустить официальный контейнер Python 3.8 Docker.

Теперь для более сложных вопросов. Следует ли обновить производственную среду до Python 3.8? Должны ли вы сделать свой собственный проект зависимым от Python 3.8, чтобы воспользоваться новыми возможностями?

У вас должно быть очень мало проблем с выполнением кода Python 3.7 в Python 3.8. Поэтому обновление среды для работы с Python 3.8 довольно безопасно, и вы сможете воспользоваться преимуществами оптимизаций, сделанных в новой версии. Различные бета-версии Python 3.8 были доступны в течение нескольких месяцев, так что, надеюсь, большинство ошибок уже исправлено. Однако, если вы хотите быть консервативным, вы можете подождать до тех пор, пока выйдет первый выпуск поддержки (Python 3.8.1).

После того, как вы обновили свою среду, вы можете начать экспериментировать с функциями, которые есть только в Python 3.8, такими как выражения присваивания и позиционные аргументы. Однако вам следует помнить о том, зависят ли другие люди от вашего кода, поскольку это также заставит их обновить свою среду. Популярные библиотеки, вероятно, будут поддерживать Python 3.6 еще довольно долго.

См. Портирование на Python 3.8 для получения дополнительной информации о подготовке вашего кода для Python 3.8.

Была ли вам полезна эта статья?
[9 / 5]

Spread the love
Подписаться
Уведомление о
guest
2 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Андрей
Андрей
5 лет назад

Круто, спасибо за перевод!

Иван
Иван
4 лет назад

Не могу лечь спать из-за твоего блога, спасибо!