Быстрее и проще: как изменился процесс разработки в Python 3.7

В июне 2018 года вышел Python 3.7. Часть изменений напрямую затрагивает рабочий процесс программиста. Кодить стало проще? Делимся впечатлениями.

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

Быстрее и проще: как изменился процесс разработки в Python 3.7

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

Узнайте о Python больше:

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ