В июне 2018 года вышел Python 3.7. Часть изменений напрямую затрагивает рабочий процесс программиста. Кодить стало проще? Делимся впечатлениями.
Обсудим функцию breakpoint()
, скоростную типизацию, возможности импорта файлов и набор консольных команд. Новый Python вам точно понравится.
breakpoint: отладка и не только
Разумеется, мы все мечтаем писать идеальный код, но давайте взглянем правде в глаза: это фантастика. Отладка – неотъемлемый и необходимый этап разработки. В Python 3.7 появилась функция breakpoint()
, которая может облегчить дебаггинг. Это не радикально новая возможность, а просто синтаксический сахар над имеющимися конструкциями.
Напишем вот такой подозрительный код и поместим его в файл bugs.py:
def divide(e, f): return f / e a, b = 0, 1 print(divide(a, b))
Сразу после запуска ловим ZeroDivisionError
внутри функции divide()
. Конечно, эту ошибку мы сделали целенаправленно, но все же постараемся ее отследить. Для этого нужно остановить программу в самом начале divide
, то есть поставить брейкпоинт. Обозначим его так:
def divide(e, f): # брейкпоинт должен быть здесь return f / e
Встретив точку останова в коде, интерпретатор затормозит процесс исполнения, что позволит нам детально разобраться в происходящем. Как именно поставить брейкпоинт? В предыдущих версиях языка нам приходилось использовать вот эту магическую конструкцию:
def divide(e, f): import pdb; pdb.set_trace() return f / e
Здесь мы импортируем стандартный дебаггер pdb и используем его для отслеживания потока выполнения. В Python 3.7 мы заменяем его на более короткую команду breakpoint()
:
def divide(e, f): breakpoint() return f / e
Под капотом происходит ровно то же, что и в нашем первом примере: импорт pdb
и обращение к методу set_trace()
. Выходит, что польза функции breakpoint()
заключается только в ее краткости? Разумеется, выигрыш существенный: 12 символов против 27. Однако реальный профит новой команды – широкие возможности для настройки.
Добавьте breakpoint()
в файл bugs.py
и запустите его:
$ python3.7 bugs.py > /home/gahjelle/bugs.py(3)divide() -> return f / e (Pdb)
Как только скрипт достигнет точки останова, он прекратит выполнение и запустит отладочную PDB-сессию. Чтобы прервать ее и вернуться, нажмите C и Enter. Чтобы лучше разобраться с отладкой и функциональностью PDB, обратитесь к этому руководству.
Настройка отладчика
Итак, вы сделали какие-то отладочные действия и думаете, что исправили ошибку. Теперь нужно снова запустить программу, но так, чтобы она не останавливалась на дебаггере. Разумеется, можно его просто закомментировать, но есть более удобный способ. Для управления отладкой используется переменная окружения PYTHONBREAKPOINT
. Если присвоить ей значение 0, то все точки останова в коде будут проигнорированы интерпретатором:
$ PYTHONBREAKPOINT=0 python3.7 bugs.py ZeroDivisionError: division by zero
Хм, кажется, баг мы все еще не пофиксили…
С помощью PYTHONBREAKPOINT
можно даже заменить PDB на другой отладчик, например, PuDB:
$ PYTHONBREAKPOINT=pudb.set_trace python3.7 bugs.py
Не забудьте установить pudb
, чтобы пример заработал:
pip install pudb
Импорт модуля Python берет на себя.
Таким образом, появилась возможность установить собственную функцию отладки. Нужно лишь присвоить ее переменной окружения PYTHONBREAKPOINT
. Если вы не знаете, как это сделать, посмотрите здесь.
Вызов любых функций
Но breakpoint
необязательно должен быть отладчиком. Например, с его помощью можно запустить сессию IPython:
$ PYTHONBREAKPOINT=IPython.embed python3.7 bugs.py IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help. In [1]: print(e / f) 0.0
Вы даже можете установить свою функцию, которую будет вызывать команда breakpoint()
. Например, этот код печатает локальные переменные. Поместите его в bp_utils.py
:
from pprint import pprint import sys def print_locals(): caller = sys._getframe(1) pprint(caller.f_locals)
А теперь обновите значение PYTHONBREAKPOINT
, используя нотацию <модуль>.<функция>:
$ PYTHONBREAKPOINT=bp_utils.print_locals python3.7 bugs.py {'e': 0, 'f': 1} ZeroDivisionError: division by zero
Обычно breakpoint
вызывает функции, которые не принимают параметры, но никто не запрещает их передавать. Давайте исправим наш отладчик в bugs.py
:
breakpoint(e, f, end="<-END\n")
Заметьте, если используется отладчик PDB, который установлен по умолчанию, вы получите TypeError
, так как его метод set_trace()
не принимает параметров.
Поэтому давайте заменим функцию отладки на print()
, чтобы понять, как это работает:
$ PYTHONBREAKPOINT=print python3.7 bugs.py 0 1<-END ZeroDivisionError: division by zero
Вы можете узнать больше о breakpoint() и sys.breakpointhook() из PEP 553.
Ускоренная типизация и предварительное объявление
Рекомендации типов улучшались во всех релизах Python 3. Теперь система типизации работает довольно стабильно. Последнее обновление также не обошло эту область стороной: добавилась поддержка ядра, в разы увеличилась производительность, появилось предварительное объявление.
Интерпретатор Python во время выполнения программы не проверяет типы данных, если, конечно, вы не используете специальные пакеты вроде enforce. Так что добавление подсказок типов, казалось бы, не должно влиять на скорость работы.
Но это не совсем так. Дело в том, что эту функциональность обеспечивает модуль typing
– один из самых медленных во всей библиотеке. Но PEP 560 ускоряет его в Python 3.7, добавив поддержку ядра. О деталях этого механизма можно не задумываться, просто наслаждайтесь скоростью.
Предварительное объявление
Система типов в Python весьма выразительна, но есть одна проблема: предварительное объявление. Все аннотации анализируются при импорте, поэтому имена должны быть указаны еще до использования. Вот такой код невозможен:
class Tree: def __init__(self, left: Tree, right: Tree) -> None: self.left = left self.right = right
Здесь мы получим NameError
, потому что нельзя ссылаться на класс Tree
в методе __init__
этого же класса, ведь он еще не определен.
Traceback (most recent call last): File "tree.py", line 1, in <module> class Tree: File "tree.py", line 2, in Tree def __init__(self, left: Tree, right: Tree) -> None: NameError: name 'Tree' is not defined
Проблему можно решить, заменив имя класса на строку:
class Tree: def __init__(self, left: "Tree", right: "Tree") -> None: self.left = left self.right = right
Обсуждение этой проблемы можно посмотреть в PEP-484.
Python 4 официально разрешит предварительное объявление. Подобные рекомендации типов не будут обрабатываться до прямого запроса. А сейчас, в Python 3.7 у нас есть __future__:
from __future__ import annotations class Tree: def __init__(self, left: Tree, right: Tree) -> None: self.left = left self.right = right
И никаких фальшивых строчных параметров.
Отложенное выполнение аннотаций ускоряет работу программы. В mypy оно уже добавлено.
Обработка аннотаций
Рассмотрим бессмысленный пример, чтобы понять, как обрабатываются аннотации при импорте. Файл anno.py
содержит следующий код:
def greet(name: print("Now!")): print(f"Hello {name}")
Мы используем print
как аннотацию для name
, чтобы понять, когда именно она будет выполняться. Импортируем этот модуль:
>>> import anno Now! >>> anno.greet.__annotations__ {'name': None} >>> anno.greet("Alice") Hello Alice
Как видно, аннотация выполнилась при импорте. Функция print
возвращает None
, поэтому name
объявлена с этим значением.
Импортируем __future__
:
from __future__ import annotations def greet(name: print("Now!")): print(f"Hello {name}")
Теперь print
не вызывается:
>>> import anno >>> anno.greet.__annotations__ {'name': "print('Now!')"} >>> anno.greet("Marty") Hello Marty
«Now!»
больше не выводится. Сама аннотация записывается в __annotations__
. Вызовем ее вручную с помощью typing.get_type_hints()
или eval()
:
>>> import typing >>> typing.get_type_hints(anno.greet) Now! {'name': <class 'NoneType'>} >>> eval(anno.greet.__annotations__["name"]) Now! >>> anno.greet.__annotations__ {'name': "print('Now!')"}
Словарь __annotations__
никогда не обновляется, поэтому вам нужно выполнять аннотацию при каждом использовании.
Простой импорт с importlib.resourses
При сборке проекта всегда возникает проблема обработки путей к файлам данных. Есть три основных варианта:
-
- Жестко задать пути;
- Добавить файлы внутрь пакета и использовать
__file__
; - Обратиться к setuptools.pkg_resources.
У каждого из них есть недостатки. Первый абсолютно не портативный. Третий ужасно медленный. Также проект может оказаться внутри zip-архива и не иметь атрибута __file__
.
В Python 3.7 появилось отличное решение – модуль importlib.resources
. Например, ваши данные хранятся в отдельном пакете следующим образом:
data/ │ ├── alice_in_wonderland.txt └── __init__.py
Пакетная структура очень важна: в папке обязательно должен находиться файл __init__.py
.
Обратиться к alice_in_wonderland.txt
теперь можно следующим образом:
>>> from importlib import resources >>> with resources.open_text("data", "alice_in_wonderland.txt") as fid: ... alice = fid.readlines() ... >>> print("".join(alice[:7])) CHAPTER I. Down the Rabbit-Hole Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, ‘and what is the use of a book,’ thought Alice ‘without pictures or conversations?’
Аналогичный метод resources.open_binary() открывает бинарные файлы. Больше узнать о новом модуле можно из выступления Barry Warsaw на PyCon 2018.
Есть возможность использовать importlib.resourses в более старых версиях языка. А здесь вы найдете инструкцию по переходу с pkg_resourses
.
Набор консольных команд
В Python 3.7 появилась новая опция -X. Документация сообщает, что она зарезервирована для различных вариантов реализации и приводит несколько примеров, определенных в CPython.
Например, с помощью команды -X importtime
можно узнать, сколько времени занимает импортирование модулей.
$ python3.7 -X importtime my_script.py import time: self [us] | cumulative | imported package import time: 2607 | 2607 | _frozen_importlib_external ... import time: 844 | 28866 | importlib.resources import time: 404 | 30434 | plugins
В столбце cumulative
отображается накопленное значение времени в микросекундах. Получается, что пакет plugins
загружался около 0.03 секунды, причем дольше всего импортировался модуль importlib.resourses
. В столбце self
можно увидеть время без учета вложенных загрузок.
Еще одна полезная команда – -X dev
. Она включает режим разработки с функциями отладки и runtime-проверки, которые не используются по умолчанию из-за своей медлительности. Также этот режим подключает модуль faulthandler.
Отметим еще одну команду -X utf8
, которая включает режим UTF-8, игнорирующий текущую локаль операционной системы. Подробное описание в PEP-540.
Оптимизации Python 3.7
Подведем небольшой итог оптимизациям в новом стандарте языка. Можно сказать, что Python наконец разогнался.
Большинство методов стандартной библиотеки теперь вызываются быстрее примерно на 20%, а сам интерпретатор тратит на треть меньше времени для старта. Самый медлительный модуль typing
ускорился в целых 7 раз! Кроме того, есть еще множество более мелких оптимизаций. В результате Python 3.7 стал действительно быстрым. На текущий момент это самая скоростная версия CPython.
Про все новшества вы можете прочитать в документации. Если хотите убедиться лично, воспользуйтесь новой командой -X importtime.
Перевод статьи Cool New Features in Python 3.7
Комментарии