Полный обзор новой версии Python 3.7

автор

Python 3.7 официально вышел! Новая версия Python была в разработке с сентября 2016 года и теперь мы все можем порадоваться результату работы команды разработчиков.

Содержание:

Что нового в этой версии Pyton? Хотя документация дает неплохое представление о новых возможностях, эта статья нацелена на более детальный осмотр главных новостей, включая:

  • Упрощенный доступ к дебагерам через встроенный breakpoint();
  • Простое создание классов при помощи классов данных;
  • Настраиваемый доступ к атрибутам модулей;
  • Улучшенная поддержка управления типами;
  • Улучшенные функции тайминга.

Самое главное, Python 3.7 стал быстрее!

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

Встроенный breakpoint()

Хотя мы и стремимся к написанию идеального кода, правда в том, что у нас это никогда не получится. Дебагинг – это важная часть программирования. Python 3.7 предоставляет новую встроенную функцию breakpoint(). Она не дает Python никакого нового функционала, но делает процесс дебагинга более интуитивным и гибким.

Предположим, что у вас есть следующий баганутый код в файле bugs.py:

Запуск кода приводит к ошибке ZeroDivisionError внутри функции divide(). Скажем, вы хотите взаимодействовать со свои кодом и перейти к дебагеру прямо в начале divide(). Вы можете сделать это, настроив т.н. «брейкпоинт» в вашем коде:

Брейкпоинт – это сигнал внутри вашего кода, чьё выполнение должно на время приостановиться, так что вы можете разобраться с текущим положением программы. Как его разместить? В Python 3.6 и ранее, мы пользовались этой странной строкой:

Здесь pdb – это дебагер Python из стандартной библиотеки. В Python 3.7 вы можете использовать вызов новой функции breakpoint() в качестве короткого пути:

За кулисами, breakpoint() сначала импортирует pdb, после чего вызывает для вас pdb.set_trace(). Очевидная польза в том, что breakpoint() проще запомнить, и нужно ввести только 12 символов, вместо 27. Однако, главный бонус в использовании breakpoint() – это простота настройки.

Запустите скрипт bugs.py вместе с breakpoint():

Скрипт будет разорван, когда дойдет до breakpoint() и перекинет вас в сессию дебагинга PDB. Вы можете вписать «с» и нажать Enter для продолжения работы скрипта. Обратитесь к руководству по PDB от Нейтана Дженнингса, если хотите узнать больше о PDB и дебагинге.

Теперь, скажем, вы думаете, что пофиксили все баги. Вам захочется запустить скрипт еще раз, но без остановки в дебагере. Вы можете, конечно, #откоментить строку breakpoint(), но второй вариант – это использовать переменную среды PYTHONBREAKPOINT. Эта переменная контролирует поведение breakpoint(), а настроив на ноль: PYTHONBREAKPOINT=0, вы сделаете каждый вызов к breakpoint() игнорируемым:

Опаньки, кажется мы так и не вылечили баг…

Еще один вариант – использовать PYTHONBREAKPOINT, чтобы определить другой дебагер, не PDB. Например, чтобы использовать PuDB (визуальный дебагер в консоли), вы можете:

Чтобы это сработала, вам нужно иметь установленный pudb.

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

Новая функция breakpoint() работает не только с дебагерами. Еще один удобный пример использования – это просто создать интерактивную оболочку внутри вашего кода. Например, чтобы начать сессию IPython, вы можете использовать следующее:

Вы также можете создать собственную функцию, и указать breakpoint(), чтобы он её вызывал. Следующий код выводит все переменные в локальном масштабе. Добавьте его в файл под названием bp_utils.py:

Чтобы использовать эту функцию, настройте PYTHONBREAKPOINT как и ранее, при помощи обозначения <module>.<function>:

Обычно, breakpoint() будет использован для вызова функций и методов, которым не нужны аргументы. Однако, вы можете передавать аргументы в том числе. Измените строк у breakpoint() в bugs.py на:

Обратите внимание: Используемый по умолчанию дебагер PDB покажет ошибку TypeError в этой строке, так как pdb.set_trace() не принимает никаких позиционных аргументов.

Запустите этот код с breakpoint(), маскирующейся под функцию print(), чтобы увидеть простой пример передачи аргументов:

Ознакомьтесь с PEP 553, а также с документацией breakpoint() и sys.breakpointhook() для дополнительной информации.

Классы данных

Новый модуль dataclasses упрощает написание собственных классов, так как специальные методы, такие как .__init__(), .__repr__() и .__eq__() добавлены автоматически. Используя декоратор @dataclass, вы можете написать что-нибудь в духе следующего:

Эти девять строк кода содержат достаточно много шаблонов проектирования. Подумайте о том, сколько может уйти времени на реализацию Country как обычного класса: метод __init__(), __repr__(), в том числе шесть разных методов сравнения, а также метод beach_per_person().

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

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

Главная цель классов данных – это писать надежные классы быстро и легко, в частности, небольшие классы, которые (в целом) рассчитаны на хранение данных.

Вы можете использовать класс данных Country как любой другой класс:

Обратите внимание на то, что поля .name, .population, .area, и .coastline используются тогда, когда инициализируется класс (кстати, .coastline – опциональный, как показано на примере Непала). Класс Country содержит (обосновано) repr, пока методы определения работают так же, как и обычные классы.

По умолчанию, классы данных можно сравнивать знаком равенства. Так как мы определили order=True в декораторе @dataclass, класс Country также может храниться:

Сортировка происходит в полях значений, сначала .name потом .population, и так далее. Однако, если вы используете field(), вы можете настроить, какие поля будут использовать в сравнении. В нашем примере, поле .area выбыло из repr и сравнений.

Обратите внимание: данные о странах из CIA World Factbook с данными о популяции населения, оцененными в июле 2017

Перед тем, как вы все ринетесь на следующие пляжные каникулы в Норвегию, вот что говорится в книге о норвежском климате: «температура умеренная вдоль побережья, образуемая Североатлантическим течением; холодный воздух с большим количеством осадков, лето холодное; круглогодичные дожди на западном побережье».

Классы данных делают то же самое, что и namedtuple. Однако, самое большое вдохновение черпается ими из проекта attrs. Для дополнительной информации, вы можете ознакомиться с документацией и PEP 557.

Настройка атрибутов модулей

Атрибуты повсюду в Python! Хотя атрибуты классов являются самыми популярными, атрибуты могут быть помещены вообще во всё – включая функции и модули. Несколько базовых функций Python реализованы в качестве атрибутов: большая часть функционала интроспекции, doc-строк и пространств имен. Функции внутри модуля делают доступными его атрибуты.

Атрибуты чаще всего добываются путем применения точечной нотации: thing.attribute. Однако, вы также можете получить атрибуты, которые названы во время выполнения при помощи getattr():

Запуск кода приведет к чему-то, на подобии этого:

В случае с классами, вызов thing.attr сначала просмотрит, является ли «attr» определенным в «thing». Если нет, тогда будет вызван специальный метод thing.__getattr__(«attr«). Метод .__getattr__() может быть использован для настройки доступа к атрибутам в объектах.

До Python 3.7, такая же настройка была не так уж легкодоступна для атрибутов модулей. Однако, PEP 562 предоставляет __getattr__() для модулей наряду с функцией __dir__(). Последняя – специальная функция, которая позволяет настраивать результаты вызова dir() в модуле.

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

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

Создайте новую директорию, «plugins», и добавьте следующий код в файл plugins/__init__.py:

Перед тем как мы ознакомимся с тем, что этот код делает, добавим еще два файла внутри директории плагина. Сначала, увидим plugins/plugin_1.py:

Далее, добавим похожий код в файл plugins/plugin_2.py:

Эти плагины могут быть использованы так

Это может не выглядеть революционно (и, наверное, таковым не является), но давайте посмотрим на то, что случилось на самом деле. Как правило, функция the hello_1() должна быть определена в модуле плагина или явно импортирована внутри __init__.py в пакете плагинов для вызова plugins.hello_1(). Здесь это не так!

Вместо этого, hello_1() определяется в произвольном файле внутри пакета плагинов, и hello_1() становится частью пакета плагинов, регистрируя себя с помощью декоратора @register_plugin.

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

Давайте быстро рассмотрим, что именно __getattr__() делает внутри кода plugins/__init__.py. Когда вы запрашиваете plugins.hello_1(), Python первым делом ищет функцию hello_1() внутри файла plugins/__init__.py. Если такой функции не существует, Python вместо этого вызывает __getattr__(«hello_1″). Запомните исходный код функции __getattr__():

__getattr__() включает в себя следующие шаги. Числа в следующем списке соответствуют нумерованным комментариям в коде:

  1. Сначала, функция оптимистично пытается получить названный плагин из словаря PLUGINS. Это пройдет успешно, если имя названного плагина существует, и уже импортировано.
  2. Если названный плагин не найден в словаре PLUGINS, нам следует убедиться в том, что все плагины импортированы.
  3. Возвращаем названный плагин, если он стал доступен после импорта.
  4. Если плагина нет в словаре PLUGINS после импорта всех плагинов, мы вызываем ошибку AttributeError, указывающую на то, что имя не является атрибутом (плагином) в данном модуле.

Как заполняется словарь PLUGINS?

Функция _import_plugins() импортирует все файлы Python внутри пакета плагинов, но не видно, чтобы она затрагивала PLUGINS:

Не забывайте о том, что каждая функция плагинов декорирована декоратором @register_plugin. Этот декоратор вызывается тогда, когда плагины импортированы, и по факту наполняет словарь PLUGINS. Вы можете увидеть это, если вы вручную импортируете один из файлов плагина:

Продолжая пример, обратите внимание на то, что вызов dir() в модуле также импортирует оставшиеся плагины:

dir() обычно ведет список всех доступных атрибутов в объекте. Обычно, использование dir() в модуле приводит к следующему:

Это может быть полезной информацией, но нам больше интересно раскрытие доступных плагинов. В Python 3.7, вы можете настроить результаты вызова dir() в модуле, добавив специальную функцию __dir__(). Для plugins/__init__.py, эта функция сначала должна убедиться в том, что все плагины были импортированы, и затем ведет список их наименований:

Перед тем как закончить с этим примером, обратите внимание на то, что мы также использовали еще одну крутую функцию Python 3.7. Чтобы импортировать все модули внутри директории плагинов, мы использовали новый модуль importlib.resources. Этот модуль дает доступ к файлам и ресурсам внутри модулей и пакетов без необходимости хакать __file__ (что не всегда работает) или pkg_resources (который работает медленно). Другие возможности importlib.resources мы рассмотрим позже.

Типизация данных

Контроль типа (видео) и аннотаций были в постоянной разработки в течение всей серии Python 3. Типизирование Python теперь стала заметно стабильнее. При этом Python 3.7 предоставляет заметные улучшения: лучшая производительность, поддержка ядра, и дальнейшие референсы.

В Python нет никакой проверки типов во время работы (если вы не используете специальные пакеты, такие как enfroce). Поэтому, добавление типизирования в ваш код не должно затронуть его производительность.

К сожалению, то, что использования типизирования требует модуль typing — не совсем правда. Модуль typing – один из самых медленных в стандартной библиотеке. PEP 560 добавляет кое-какую поддержку ядра для типизирования в Python 3.7, что значительно ускоряет модуль typing. В данном случае нам не обязательно вникать в детали. Просто откиньтесь в кресле, и наслаждайтесь улучшенной производительностью.

Так как типизирование в Python объяснимо впечатляет, есть небольшая проблема – это предварительное объявление. Типизирование – оцениваются во время импорта модуля. Поэтому все имена уже должны быть определены перед использованием. Мы не можем выполнить следующее:

Запуск кода приводит к ошибке NameError, так как класс Tree еще не до конца определен в определении метода .__init__():

Чтобы обойти это, вам нужно вписать «Tree» в виде строкового литерала вместо:

Вы можете ознакомиться с PEP 484 для дополнительной информации.

В будущем Python 4.0, так называемое предварительное объявление будет доступна. Они будут обработаны не отслеживаемыми аннотациями до тех пор, пока отслеживание не будет запрошено напрямую. В PEP 563 объясняется, какие имеются детали у этого предложения. В Python 3.7, предварительное объявление уже доступна в качестве импорта __future__. Вы можете попробовать следующее:

Обратите внимание на то, чтобы избежать неуклюжего синтаксиса «Tree», отложенная выполнение аннотаций должна также ускорить ваш код, так как контроль типов не выполняются. Предварительное объявление уже поддерживаются в mypy.

Безусловно, наиболее частое использование аннотаций – это типизация. Тем не менее, у вас все еще есть доступ к аннотациям во время выполнения, и вы можете использовать их по своему усмотрению. Если вы обрабатываете аннотации напрямую, вам придется иметь дело с предварительном объявлении.

Давайте сделаем пару дурацких примеров, которые показывают, когда аннотации выполняются. Для начала пойдем по старинке, так что аннотации выполняются во время импорта. Разместим в anno.py следующий код:

Обратите внимание на то, что аннотация для «name» – это print(). Это нужно только для того, чтобы увидеть, когда именно аннотация выполняется:

Как мы видим, аннотация была выполнена во время импорта. Обратите внимание также на то, что «name» заканчивается аннотированным None, так как это то что возвращает print().

Добавим импорт __future__ для включения отложенного выполнения аннотаций:

Импорт этого обновленного кода не будет выполнять аннотацию:

Обратите внимание на то, что «Now!» никогда не выводится и аннотация хранится в качестве литеральной строки в словаре __annotations__. Чтобы выполнить аннотацию, используем typing.get_type_hints() или eval():

Мы видим, что словарь __annotations__ так и не был обновлен, так что нам нужно выполнить аннотацию каждый раз, когда вы используете его.

Точный тайминг

В Python 3.7, модуль time получил несколько новых функций, как сказано в PEP 564. Если быть точным, были добавлены следующие шесть функций:

  • clock_gettime_ns(): Возвращает время определенных часов;
  • clock_settime_ns(): Устанавливает время определенных часов;
  • monotonic_ns(): Возвращает время относительных часов, которые не могут откатываться назад (например, из-за летнего времени);
  • perf_counter_ns(): Возвращает значение счетчика производительности – часов, специально разработанных для подсчета коротких интервалов;
  • process_time_ns(): Возвращает сумму работы системы и пользователя для текущего процесса (спящий режим не учитывается);
  • time_ns(): Возвращает количество наносекунд, начиная с первого января 1970 года.

В каком-то смысле, нового функционала мы так и не видим. Каждая функция аналогична уже существующим функциям, только без суффикса _ns. Разница в том, что новая функция возвращает количество наносекунд в качестве int, вместо количества секунд в качестве float.

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

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

Запятая в Python следует стандарту IEEE 754 и использует целых 53 ценных байта. Результат заключается в том, что любое время, длящееся дольше 104 дней (2⁵³, или 9 квадриллионов наносекунд) не может быть выражено через запятую с точностью до наносекунд. Напротив, int в Python не ограничен, так что целое число наносекунд всегда будет с наносекундной точностью, не зависимости от количества времени.

К примеру, time.time() возвращается количество секунд, начиная с 1 января 1970 года. Это число уже очень большое, так как точность этого числа представлена на уровне микросекунд. Эта функция показывает себя лучше всего в качестве улучшений в своей _ns версии. Разрешение time.time_ns() примерно в 3 раза лучше, чем у time.time().

Что такое наносекунда, кстати? Технически – это одна миллиардная часть секунды, или 1е-9 секунд, если вы предпочитаете научный формат. Это просто цифры. Для лучшего понимания наносекунд, можете ознакомиться с великолепной демонстрацией Грейс Хоппер.

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

Вместо этого, вы можете использовать проект astropy. Его пакет astropy.time представляет дату, используя два объекта запятых, которые гарантируют «субнаносекундную точность измерения времени, охватывающую возраст вселенной».

Последняя версия astropy доступна в Python 3.5+

Еще больше интересного в Python 3.7

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

Гарантированный порядок словарей

CPython в Python 3.6 имел упорядоченные словари, как и PyPy. Это значит, что содержимое словарей повторяется в том же порядке, в котором оно было внесено. В первом примере мы используем Python 3.5, а во втором – Python 3.6:

В Python 3.6, данный порядок был просто приятным следствием реализации словаря. В Python 3.7 словари, сохраняющие свой порядок вставки, являются частью спецификации языка. Таким образом, на это можно полагаться в проектах, которые поддерживают только Python >= 3.7 (или CPython >= 3.6).

async” и “await” теперь ключевые слова

Python 3.5 предоставил сопрограммы с синтаксом async и await. Чтобы избежать проблем, связанных с обратной совместимостью, async и await не были внесены в список резервных ключевых слов. Другими словами, все еще можно было определить переменные или функции с именем async и await.

В Python 3.7 так больше нельзя:

Подтяжка “asyncio

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

В Python 3.7, модуль asyncio был заметно обновлен: добавлены новые функции, поддержка контекстных переменных (см. ниже) и улучшение производительности.

Отдельного внимания заслуживает asyncio.run(), которая упрощает вызов сопрограмм из синхронного кода. Используя asyncio.run(), вам больше не нужно отдельно создавать цикл событий. Асинхронная программа «Hello World» теперь может быть написана следующим образом:

Контекстные переменные

Контекстные переменные – это переменные, которые могут иметь разные значения в соответствии со своим контекстом. Они аналогичны ThreadLocal Storage, в которых каждый поток имеет разное значение для переменное. Однако, с контекстными переменными может быть несколько контекстов в одном выполняемом потоке. Контекстные переменные главным образом используются для отслеживания переменных в параллельных асинхронных задачах.

Следующий пример составляет три контекста, каждый из которых имеет собственное значение для наименования этого значения. Функция greet() позже может использовать значение имени внутри каждого контекста:

Выполняем данный код:

Импорт файлов данных с “importlib.resources“

Одна из проблем при упаковке проекта Python – это выбор того, что делать с проектными ресурсами, такими как файлы данных проекта. Есть несколько привычных вариантов действий:

  • Мучатся с путем к файлу данных;
  • Внесение файла данных в пакет и искать его при помощи__file__.;
  • Использовать setuptools.pkg_resources для получения ресурса файла данных.

Каждый из этих путей имеет свои недостатки. Первый вариант не является портативным. Использование __file__ — более портативно, но если проект Python установлен, можно оказаться в ситуации с zip архивом без атрибута __file__. Третий вариант решает эту проблему, но он удручающие медленный.

Лучшее решение – это новый модуль importlib.resources в стандартной библиотеке. Он использует существующий функционал импорта Python, чтобы импортировать, в том числе, и файлы данных. Представим, что у вас есть ресурс внутри пакета Python, вот таким образом:

Обратите внимание на то, что данные должны быть пакетом Python. Таким образом, директория должна хранить файл __init__.py (который может быть пустым). После этого, вы можете читать файл alice_in_wonderland.txt, вот так:

Похожая функция resources.open_binary() доступна для открытия файлов в бинарном режиме. В раннем примере «плагины как атрибуты модулей», мы использовали importlib.resources для исследования доступных плагинов при помощи resources.contents(). Вы можете ознакомиться с речью Берри Ворсоу на PyCon в 2018 году для дополнительной информации.

Мы можем использовать importlib.resources в Python 2.7 и Python 3.4+ через backport. Вы также можете найти инструкции по миграции из pkg_resources в importlib.resources.

Хитрости разработчиков

Python 3.7 получил новые функции, нацеленные на вас, как разработчика. Вы уже ознакомились с новеньким встроенным breakpoint(). В дополнение, несколько новых опций командной строки –Х также были добавлены в интерпретатор Python.

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

В кумулятивной колонке показано кумулятивное время импорт (в микросекундах). В данном примере, импортирование плагинов заняло примерно 0.03 секунд, большая часть которых потрачена на импорт importlib.resources. Колонка self показывает время, потраченное на импорт, не считая вложенный импорт.

Вы можете использовать X dev, чтобы активировать «режим разработки». Этот режим добавляет определенные функции дебагинга и проверки запусков, которые посчитали слишком медленными, чтобы подключать по умолчанию. Это включает в себя подключение faulthandler, чтобы показывать результаты отслеживаний серьезных крешей, наряду с другими предупреждениями.

Наконец, X utf8 включает режим UTF-8. В этом режиме, UTF-8 будет использоваться для текстового кодирования, вне зависимости от текущей локали.

Оптимизации

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

  • Меньше расходов при вызове множества методов в стандартной библиотеке.
  • В целом, методы вызываются на 20% быстрее.
  • Время запуска Python ускорено на 10-30%
  • Ввод импорта в 7 раз быстрее.

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

Результат этих оптимизаций очевиден – Python 3.7 работает быстро. Это самая быстрая версия CPython на данный момент.

Стоит ли мне обновляться?

Давайте начнем с простого ответа. Если вы хотите пробовать новые функции, с которыми вы ознакомились в данной статье, то вам нужно иметь возможность использовать Python 3.7. Использование таких инструментов, как pyenv или Anaconda упрощает факт наличия нескольких версий Python, установленных параллельно. Нет ничего плохого в установке Python 3.7 и попробовать работать с ним.

Перейдем к более сложным вопросам. Стоит ли вам обновлять вашу производственную среду под Python 3.7? Стоит ли вам делать ваш проект зависимым от Python 3.7, чтобы попробовать новые функции?

Начну с очевидного предупреждения: вы всегда должны проводить тщательное тестирование, перед тем как обновлять вашу производственную среду. В Python 3.7 всего несколько вещей, которые могут сломать старый код (async и await стали ключевыми словами – это один из примеров). Если вы уже используете современный Python, обновление до 3.7 будет пустяком. Если вы хотите побыть более консервативным, вы можете захотеть дождаться следующего релиза – Python 3.7.1, который ожидается в июле 2018.

Спорить на тему того, что вы должны вести свой проект на Python 3.7, конечно, сложнее. Множество функций Python 3.7 либо доступны в качестве бекпортов в Python 3.6 (классы данных, importlib.resources), либо являются более удобными (более быстрый запуск и вызов методов, упрощенный дебагинг, опции –Х). В конце концов, вы можете попробовать запустить Python 3.7 лично и беречь свой код для Python 3.6 (или ранее).

Крупные функции, которые привяжут ваш код к Python 3.7 — это __getattr__() в модулях, предварительное объявление в аннотациях и функции тайминга в наносекундах. Если вы хотите внедрить что-либо из перечисленного, вам нужно идти вперед и выполнить свои требования. В противном случае, ваш проект будет более полезным для других, если он может быть запущен в Python 3.6 еще какое-то время.

Вам может быть интересно

Scroll Up