Python 3.7 официально вышел! Новая версия Python была в разработке с сентября 2016 года и теперь мы все можем порадоваться результату работы команды разработчиков.
Содержание:
- Встроенный breakpoint()
- Классы Данных
- Настройка атрибутов модулей
- Типизация данных
- Точный тайминг
- Больше интересного в Python 3.7
- Гарантированный порядок словарей
- “async” и “await” теперь ключевые слова
- Подтяжка “asyncio”
- Контекстные переменные
- Импорт данных файлов при помощи importlib.resources“
- Хитрости разработчиков
- Оптимизации
- Стоит ли мне обновляться?
Что нового в этой версии Pyton? Хотя документация дает неплохое представление о новых возможностях, эта статья нацелена на более детальный осмотр главных новостей, включая:
- Упрощенный доступ к дебагерам через встроенный breakpoint();
- Простое создание классов при помощи классов данных;
- Настраиваемый доступ к атрибутам модулей;
- Улучшенная поддержка управления типами;
- Улучшенные функции тайминга.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Самое главное, Python 3.7 стал быстрее!
В последнем разделе этой статьи вы узнаете больше о его скорости, также как и о других функциях Python 3.7. Мы также рассмотрим несколько советов по обновлению до последней версии.
Встроенный breakpoint()
Хотя мы и стремимся к написанию идеального кода, правда в том, что у нас это никогда не получится. Дебагинг – это важная часть программирования. Python 3.7 предоставляет новую встроенную функцию breakpoint(). Она не дает Python никакого нового функционала, но делает процесс дебагинга более интуитивным и гибким.
Предположим, что у вас есть следующий баганутый код в файле bugs.py:
1 2 3 4 5 |
def divide(e, f): return f / e a, b = 0, 1 print(divide(a, b)) |
Запуск кода приводит к ошибке ZeroDivisionError внутри функции divide(). Скажем, вы хотите взаимодействовать со свои кодом и перейти к дебагеру прямо в начале divide(). Вы можете сделать это, настроив т.н. «брейкпоинт» в вашем коде:
1 2 3 |
def divide(e, f): # Вставляем брейкпоинт здесь return f / e |
Брейкпоинт – это сигнал внутри вашего кода, чьё выполнение должно на время приостановиться, так что вы можете разобраться с текущим положением программы. Как его разместить? В Python 3.6 и ранее, мы пользовались этой странной строкой:
1 2 3 |
def divide(e, f): import pdb; pdb.set_trace() return f / e |
Здесь pdb – это дебагер Python из стандартной библиотеки. В Python 3.7 вы можете использовать вызов новой функции breakpoint() в качестве короткого пути:
1 2 3 |
def divide(e, f): breakpoint() return f / e |
За кулисами, breakpoint() сначала импортирует pdb, после чего вызывает для вас pdb.set_trace(). Очевидная польза в том, что breakpoint() проще запомнить, и нужно ввести только 12 символов, вместо 27. Однако, главный бонус в использовании breakpoint() – это простота настройки.
Запустите скрипт bugs.py вместе с breakpoint():
1 2 3 4 |
$ python3.7 bugs.py > /home/gahjelle/bugs.py(3)divide() -> return f / e (Pdb) |
Скрипт будет разорван, когда дойдет до breakpoint() и перекинет вас в сессию дебагинга PDB. Вы можете вписать «с» и нажать Enter для продолжения работы скрипта. Обратитесь к руководству по PDB от Нейтана Дженнингса, если хотите узнать больше о PDB и дебагинге.
Теперь, скажем, вы думаете, что пофиксили все баги. Вам захочется запустить скрипт еще раз, но без остановки в дебагере. Вы можете, конечно, #откоментить строку breakpoint(), но второй вариант – это использовать переменную среды PYTHONBREAKPOINT. Эта переменная контролирует поведение breakpoint(), а настроив на ноль: PYTHONBREAKPOINT=0, вы сделаете каждый вызов к breakpoint() игнорируемым:
1 2 |
$ PYTHONBREAKPOINT=0 python3.7 bugs.py ZeroDivisionError: division by zero |
Опаньки, кажется мы так и не вылечили баг…
Еще один вариант – использовать PYTHONBREAKPOINT, чтобы определить другой дебагер, не PDB. Например, чтобы использовать PuDB (визуальный дебагер в консоли), вы можете:
1 |
$ PYTHONBREAKPOINT=pudb.set_trace python3.7 bugs.py |
Чтобы это сработала, вам нужно иметь установленный pudb.
1 |
pip install pudb |
Python позаботится об импорте pudb для вас. Таким же образом вы можете указать ваш дебагер по умолчанию. Просто настройте переменную среды PYTHONBREAKPOINT на тот дебагер, который вы предпочитаете. Вы можете ознакомиться с инструкциями о настройке переменной среды на вашей системе.
Новая функция breakpoint() работает не только с дебагерами. Еще один удобный пример использования – это просто создать интерактивную оболочку внутри вашего кода. Например, чтобы начать сессию IPython, вы можете использовать следующее:
1 2 3 4 5 |
$ 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:
1 2 3 4 5 6 |
from pprint import pprint import sys def print_locals(): caller = sys._getframe(1) # Caller на 1 фрейм выше pprint(caller.f_locals) |
Чтобы использовать эту функцию, настройте PYTHONBREAKPOINT как и ранее, при помощи обозначения <module>.<function>:
1 2 3 |
$ PYTHONBREAKPOINT=bp_utils.print_locals python3.7 bugs.py {'e': 0, 'f': 1} ZeroDivisionError: division by zero |
Обычно, breakpoint() будет использован для вызова функций и методов, которым не нужны аргументы. Однако, вы можете передавать аргументы в том числе. Измените строк у breakpoint() в bugs.py на:
1 |
breakpoint(e, f, end="<-END\n") |
Обратите внимание: Используемый по умолчанию дебагер PDB покажет ошибку TypeError в этой строке, так как pdb.set_trace() не принимает никаких позиционных аргументов.
Запустите этот код с breakpoint(), маскирующейся под функцию print(), чтобы увидеть простой пример передачи аргументов:
1 2 3 |
$ PYTHONBREAKPOINT=print python3.7 bugs.py 0 1<-END ZeroDivisionError: division by zero |
Ознакомьтесь с PEP 553, а также с документацией breakpoint() и sys.breakpointhook() для дополнительной информации.
Классы данных
Новый модуль dataclasses упрощает написание собственных классов, так как специальные методы, такие как .__init__(), .__repr__() и .__eq__() добавлены автоматически. Используя декоратор @dataclass, вы можете написать что-нибудь в духе следующего:
1 2 3 4 5 6 7 8 9 10 11 12 |
from dataclasses import dataclass, field @dataclass(order=True) class Country: name: str population: int area: float = field(repr=False, compare=False) coastline: float = 0 def beach_per_person(self): """Метры береговой линии на человека""" return (self.coastline * 1000) / self.population |
Эти девять строк кода содержат достаточно много шаблонов проектирования. Подумайте о том, сколько может уйти времени на реализацию Country как обычного класса: метод __init__(), __repr__(), в том числе шесть разных методов сравнения, а также метод beach_per_person().
Вы можете посмотреть на предоставленный ниже код, чтобы увидеть реализацию Country, который является грубым эквивалентом класса без использования dataclass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
class Country: def __init__(self, name, population, area, coastline=0): self.name = name self.population = population self.area = area self.coastline = coastline def __repr__(self): return ( f"Country(name={self.name!r}, population={self.population!r}," f" coastline={self.coastline!r})" ) def __eq__(self, other): if other.__class__ is self.__class__: return ( (self.name, self.population, self.coastline) == (other.name, other.population, other.coastline) ) return NotImplemented def __ne__(self, other): if other.__class__ is self.__class__: return ( (self.name, self.population, self.coastline) != (other.name, other.population, other.coastline) ) return NotImplemented def __lt__(self, other): if other.__class__ is self.__class__: return ((self.name, self.population, self.coastline) < ( other.name, other.population, other.coastline )) return NotImplemented def __le__(self, other): if other.__class__ is self.__class__: return ((self.name, self.population, self.coastline) <= ( other.name, other.population, other.coastline )) return NotImplemented def __gt__(self, other): if other.__class__ is self.__class__: return ((self.name, self.population, self.coastline) > ( other.name, other.population, other.coastline )) return NotImplemented def __ge__(self, other): if other.__class__ is self.__class__: return ((self.name, self.population, self.coastline) >= ( other.name, other.population, other.coastline )) return NotImplemented def beach_per_person(self): """Метры береговой линии на человека""" return (self.coastline * 1000) / self.population |
После создания, класс данных – это нормальный класс. Вы можете, к примеру, наследовать из класса данных обычным способом.
Главная цель классов данных – это писать надежные классы быстро и легко, в частности, небольшие классы, которые (в целом) рассчитаны на хранение данных.
Вы можете использовать класс данных Country как любой другой класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>>> norway = Country("Norway", 5320045, 323802, 58133) >>> norway Country(name='Norway', population=5320045, coastline=58133) >>> norway.area 323802 >>> usa = Country("United States", 326625791, 9833517, 19924) >>> nepal = Country("Nepal", 29384297, 147181) >>> nepal Country(name='Nepal', population=29384297, coastline=0) >>> usa.beach_per_person() 0.06099946957342386 >>> norway.beach_per_person() 10.927163210085629 |
Обратите внимание на то, что поля .name, .population, .area, и .coastline используются тогда, когда инициализируется класс (кстати, .coastline – опциональный, как показано на примере Непала). Класс Country содержит (обосновано) repr, пока методы определения работают так же, как и обычные классы.
По умолчанию, классы данных можно сравнивать знаком равенства. Так как мы определили order=True в декораторе @dataclass, класс Country также может храниться:
1 2 3 4 5 6 7 8 9 10 |
>>> norway == norway True >>> nepal == usa False >>> sorted((norway, usa, nepal)) [Country(name='Nepal', population=29384297, coastline=0), Country(name='Norway', population=5320045, coastline=58133), Country(name='United States', population=326625791, coastline=19924)] |
Сортировка происходит в полях значений, сначала .name потом .population, и так далее. Однако, если вы используете field(), вы можете настроить, какие поля будут использовать в сравнении. В нашем примере, поле .area выбыло из repr и сравнений.
Обратите внимание: данные о странах из CIA World Factbook с данными о популяции населения, оцененными в июле 2017
Перед тем, как вы все ринетесь на следующие пляжные каникулы в Норвегию, вот что говорится в книге о норвежском климате: «температура умеренная вдоль побережья, образуемая Североатлантическим течением; холодный воздух с большим количеством осадков, лето холодное; круглогодичные дожди на западном побережье».
Классы данных делают то же самое, что и namedtuple. Однако, самое большое вдохновение черпается ими из проекта attrs. Для дополнительной информации, вы можете ознакомиться с документацией и PEP 557.
Настройка атрибутов модулей
Атрибуты повсюду в Python! Хотя атрибуты классов являются самыми популярными, атрибуты могут быть помещены вообще во всё – включая функции и модули. Несколько базовых функций Python реализованы в качестве атрибутов: большая часть функционала интроспекции, doc-строк и пространств имен. Функции внутри модуля делают доступными его атрибуты.
Атрибуты чаще всего добываются путем применения точечной нотации: thing.attribute. Однако, вы также можете получить атрибуты, которые названы во время выполнения при помощи getattr():
1 2 3 4 5 6 7 8 9 |
import random random_attr = random.choice( ("gammavariate", "lognormvariate", "normalvariate") ) random_func = getattr(random, random_attr) print(f"A {random_attr} random value: {random_func(1, 1)}") |
Запуск кода приведет к чему-то, на подобии этого:
1 |
A gammavariate random value: 2.8017715125270618 |
В случае с классами, вызов thing.attr сначала просмотрит, является ли «attr» определенным в «thing». Если нет, тогда будет вызван специальный метод thing.__getattr__(«attr«). Метод .__getattr__() может быть использован для настройки доступа к атрибутам в объектах.
До Python 3.7, такая же настройка была не так уж легкодоступна для атрибутов модулей. Однако, PEP 562 предоставляет __getattr__() для модулей наряду с функцией __dir__(). Последняя – специальная функция, которая позволяет настраивать результаты вызова dir() в модуле.
В самом PEP предоставлены простые примеры того, как эти функции могут быть использованы, включая добавление предупреждений об устаревании функций и медленной загрузки, больших модулей.
Ниже, мы построим простую систему плагинов, которая позволит функциям быть динамически добавленными в модуль.
Создайте новую директорию, «plugins», и добавьте следующий код в файл plugins/__init__.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
from importlib import import_module from importlib import resources PLUGINS = dict() def register_plugin(func): """Декоратор для регистрации плагина""" name = func.__name__ PLUGINS[name] = func return func def __getattr__(name): """Возвращает имя плагина""" try: return PLUGINS[name] except KeyError: _import_plugins() if name in PLUGINS: return PLUGINS[name] else: raise AttributeError( f"module {__name__!r} has no attribute {name!r}" ) from None def __dir__(): """Список доступных плагинов""" _import_plugins() return list(PLUGINS.keys()) def _import_plugins(): """Импортируем все ресурсы для регистрации плагина""" for name in resources.contents(__name__): if name.endswith(".py"): import_module(f"{__name__}.{name[:-3]}") |
Перед тем как мы ознакомимся с тем, что этот код делает, добавим еще два файла внутри директории плагина. Сначала, увидим plugins/plugin_1.py:
1 2 3 4 5 |
from . import register_plugin @register_plugin def hello_1(): print("Hello from Plugin 1") |
Далее, добавим похожий код в файл plugins/plugin_2.py:
1 2 3 4 5 6 7 8 9 |
from . import register_plugin @register_plugin def hello_2(): print("Hello from Plugin 2") @register_plugin def goodbye(): print("Plugin 2 says goodbye") |
Эти плагины могут быть использованы так
1 2 3 4 5 6 7 8 9 |
>>> import plugins >>> plugins.hello_1() Hello from Plugin 1 >>> dir(plugins) ['goodbye', 'hello_1', 'hello_2'] >>> plugins.goodbye() Plugin 2 says goodbye |
Это может не выглядеть революционно (и, наверное, таковым не является), но давайте посмотрим на то, что случилось на самом деле. Как правило, функция 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__():
1 2 3 4 5 6 7 8 9 10 11 12 |
def __getattr__(name): """Return a named plugin""" try: return PLUGINS[name] # 1) Пробуем вернуть плагин except KeyError: _import_plugins() # 2) Импорт всех плагинов if name in PLUGINS: return PLUGINS[name] # 3) Пробуем вернуть плагин еще раз else: raise AttributeError( # 4) Возвращаем ошибку f"module {__name__!r} has no attribute {name!r}" ) from None |
__getattr__() включает в себя следующие шаги. Числа в следующем списке соответствуют нумерованным комментариям в коде:
- Сначала, функция оптимистично пытается получить названный плагин из словаря PLUGINS. Это пройдет успешно, если имя названного плагина существует, и уже импортировано.
- Если названный плагин не найден в словаре PLUGINS, нам следует убедиться в том, что все плагины импортированы.
- Возвращаем названный плагин, если он стал доступен после импорта.
- Если плагина нет в словаре PLUGINS после импорта всех плагинов, мы вызываем ошибку AttributeError, указывающую на то, что имя не является атрибутом (плагином) в данном модуле.
Как заполняется словарь PLUGINS?
Функция _import_plugins() импортирует все файлы Python внутри пакета плагинов, но не видно, чтобы она затрагивала PLUGINS:
1 2 3 4 5 |
def _import_plugins(): """Импортируем все ресурсы для регистрации плагина""" for name in resources.contents(__name__): if name.endswith(".py"): import_module(f"{__name__}.{name[:-3]}") |
Не забывайте о том, что каждая функция плагинов декорирована декоратором @register_plugin. Этот декоратор вызывается тогда, когда плагины импортированы, и по факту наполняет словарь PLUGINS. Вы можете увидеть это, если вы вручную импортируете один из файлов плагина:
1 2 3 4 5 6 7 |
>>> import plugins >>> plugins.PLUGINS {} >>> import plugins.plugin_1 >>> plugins.PLUGINS {'hello_1': <function hello_1 at 0x7f29d4341598>} |
Продолжая пример, обратите внимание на то, что вызов dir() в модуле также импортирует оставшиеся плагины:
1 2 3 4 5 6 7 |
>>> dir(plugins) ['goodbye', 'hello_1', 'hello_2'] >>> plugins.PLUGINS {'hello_1': <function hello_1 at 0x7f29d4341598>, 'hello_2': <function hello_2 at 0x7f29d4341620>, 'goodbye': <function goodbye at 0x7f29d43416a8>} |
dir() обычно ведет список всех доступных атрибутов в объекте. Обычно, использование dir() в модуле приводит к следующему:
1 2 3 4 5 6 |
>>> import plugins >>> dir(plugins) ['PLUGINS', '__builtins__', '__cached__', '__doc__', '__file__', '__getattr__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '_import_plugins', 'import_module', 'register_plugin', 'resources'] |
Это может быть полезной информацией, но нам больше интересно раскрытие доступных плагинов. В Python 3.7, вы можете настроить результаты вызова dir() в модуле, добавив специальную функцию __dir__(). Для plugins/__init__.py, эта функция сначала должна убедиться в том, что все плагины были импортированы, и затем ведет список их наименований:
1 2 3 4 |
def __dir__(): """Список доступных плагинов""" _import_plugins() return list(PLUGINS.keys()) |
Перед тем как закончить с этим примером, обратите внимание на то, что мы также использовали еще одну крутую функцию 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 объяснимо впечатляет, есть небольшая проблема – это предварительное объявление. Типизирование – оцениваются во время импорта модуля. Поэтому все имена уже должны быть определены перед использованием. Мы не можем выполнить следующее:
1 2 3 4 |
class Tree: def __init__(self, left: Tree, right: Tree) -> None: self.left = left self.right = right |
Запуск кода приводит к ошибке NameError, так как класс Tree еще не до конца определен в определении метода .__init__():
1 2 3 4 5 6 |
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 |
Чтобы обойти это, вам нужно вписать «Tree» в виде строкового литерала вместо:
1 2 3 4 |
class Tree: def __init__(self, left: "Tree", right: "Tree") -> None: self.left = left self.right = right |
Вы можете ознакомиться с PEP 484 для дополнительной информации.
В будущем Python 4.0, так называемое предварительное объявление будет доступна. Они будут обработаны не отслеживаемыми аннотациями до тех пор, пока отслеживание не будет запрошено напрямую. В PEP 563 объясняется, какие имеются детали у этого предложения. В Python 3.7, предварительное объявление уже доступна в качестве импорта __future__. Вы можете попробовать следующее:
1 2 3 4 5 6 |
from __future__ import annotations class Tree: def __init__(self, left: Tree, right: Tree) -> None: self.left = left self.right = right |
Обратите внимание на то, чтобы избежать неуклюжего синтаксиса «Tree», отложенная выполнение аннотаций должна также ускорить ваш код, так как контроль типов не выполняются. Предварительное объявление уже поддерживаются в mypy.
Безусловно, наиболее частое использование аннотаций – это типизация. Тем не менее, у вас все еще есть доступ к аннотациям во время выполнения, и вы можете использовать их по своему усмотрению. Если вы обрабатываете аннотации напрямую, вам придется иметь дело с предварительном объявлении.
Давайте сделаем пару дурацких примеров, которые показывают, когда аннотации выполняются. Для начала пойдем по старинке, так что аннотации выполняются во время импорта. Разместим в anno.py следующий код:
1 2 |
def greet(name: print("Now!")): print(f"Hello {name}") |
Обратите внимание на то, что аннотация для «name» – это print(). Это нужно только для того, чтобы увидеть, когда именно аннотация выполняется:
1 2 3 4 5 6 7 8 |
>>> import anno Now! >>> anno.greet.__annotations__ {'name': None} >>> anno.greet("Alice") Hello Alice |
Как мы видим, аннотация была выполнена во время импорта. Обратите внимание также на то, что «name» заканчивается аннотированным None, так как это то что возвращает print().
Добавим импорт __future__ для включения отложенного выполнения аннотаций:
1 2 3 4 |
from __future__ import annotations def greet(name: print("Now!")): print(f"Hello {name}") |
Импорт этого обновленного кода не будет выполнять аннотацию:
1 2 3 4 5 6 7 |
>>> import anno >>> anno.greet.__annotations__ {'name': "print('Now!')"} >>> anno.greet("Marty") Hello Marty |
Обратите внимание на то, что «Now!» никогда не выводится и аннотация хранится в качестве литеральной строки в словаре __annotations__. Чтобы выполнить аннотацию, используем typing.get_type_hints() или eval():
1 2 3 4 5 6 7 8 9 10 |
>>> 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__ так и не был обновлен, так что нам нужно выполнить аннотацию каждый раз, когда вы используете его.
Точный тайминг
В 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. Числа с плавающей запятой по природе своей являются неточными:
1 2 3 4 5 |
>>> 0.1 + 0.1 + 0.1 0.30000000000000004 >>> 0.1 + 0.1 + 0.1 == 0.3 False |
Это не проблема для Python, скорее для компьютера, так компьютерам нужно представлять бесконечные десятичные числа, используя ограниченное количество байтов.
Запятая в Python следует стандарту IEEE 754 и использует целых 53 ценных байта. Результат заключается в том, что любое время, длящееся дольше 104 дней (2⁵³, или 9 квадриллионов наносекунд) не может быть выражено через запятую с точностью до наносекунд. Напротив, int в Python не ограничен, так что целое число наносекунд всегда будет с наносекундной точностью, не зависимости от количества времени.
К примеру, time.time() возвращается количество секунд, начиная с 1 января 1970 года. Это число уже очень большое, так как точность этого числа представлена на уровне микросекунд. Эта функция показывает себя лучше всего в качестве улучшений в своей _ns версии. Разрешение time.time_ns() примерно в 3 раза лучше, чем у time.time().
Что такое наносекунда, кстати? Технически – это одна миллиардная часть секунды, или 1е-9 секунд, если вы предпочитаете научный формат. Это просто цифры. Для лучшего понимания наносекунд, можете ознакомиться с великолепной демонстрацией Грейс Хоппер.
Не для протокола: если вы хотите работать с датами с точностью до наносекунд, стандартная библиотека datetime не будет их обрезать. Она точно обрабатывает только микросекунды:
1 2 3 4 5 6 |
>>> from datetime import datetime, timedelta >>> datetime(2018, 6, 27) + timedelta(seconds=1e-6) datetime.datetime(2018, 6, 27, 0, 0, 0, 1) >>> datetime(2018, 6, 27) + timedelta(seconds=1e-9) datetime.datetime(2018, 6, 27, 0, 0) |
Вместо этого, вы можете использовать проект astropy. Его пакет astropy.time представляет дату, используя два объекта запятых, которые гарантируют «субнаносекундную точность измерения времени, охватывающую возраст вселенной».
1 2 3 4 5 6 7 |
>>> from astropy.time import Time, TimeDelta >>> Time("2018-06-27") <Time object: scale='utc' format='iso' value=2018-06-27 00:00:00.000> >>> t = Time("2018-06-27") + TimeDelta(1e-9, format="sec") >>> (t - Time("2018-06-27")).sec 9.976020010071807e-10 |
Последняя версия astropy доступна в Python 3.5+
Еще больше интересного в Python 3.7
Наверное, вы уже видели заголовки новостей о Python 3.7. Однако, мы имеем дело с гораздо большим количеством изменений, которые также стоит отметить. В данном разделе, мы кратко пройдемся и по ним.
Гарантированный порядок словарей
CPython в Python 3.6 имел упорядоченные словари, как и PyPy. Это значит, что содержимое словарей повторяется в том же порядке, в котором оно было внесено. В первом примере мы используем Python 3.5, а во втором – Python 3.6:
1 2 3 4 5 |
>>> {"one": 1, "two": 2, "three": 3} # Python <= 3.5 {'three': 3, 'one': 1, 'two': 2} >>> {"one": 1, "two": 2, "three": 3} # Python >= 3.6 {'one': 1, 'two': 2, 'three': 3} |
В 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 так больше нельзя:
1 2 3 4 5 6 7 8 9 10 11 |
>>> async = 1 File "<stdin>", line 1 async = 1 ^ SyntaxError: invalid syntax >>> def await(): File "<stdin>", line 1 def await(): ^ SyntaxError: invalid syntax |
Подтяжка “asyncio”
Стандартная библиотека asyncio изначально была представлена в Python 3.4 для современной обработки параллелизма с использованием циклов событий, сопрограмм и фьючерсов.
В Python 3.7, модуль asyncio был заметно обновлен: добавлены новые функции, поддержка контекстных переменных (см. ниже) и улучшение производительности.
Отдельного внимания заслуживает asyncio.run(), которая упрощает вызов сопрограмм из синхронного кода. Используя asyncio.run(), вам больше не нужно отдельно создавать цикл событий. Асинхронная программа «Hello World» теперь может быть написана следующим образом:
1 2 3 4 5 6 |
import asyncio async def hello_world(): print("Hello World!") asyncio.run(hello_world()) |
Контекстные переменные
Контекстные переменные – это переменные, которые могут иметь разные значения в соответствии со своим контекстом. Они аналогичны Thread—Local Storage, в которых каждый поток имеет разное значение для переменное. Однако, с контекстными переменными может быть несколько контекстов в одном выполняемом потоке. Контекстные переменные главным образом используются для отслеживания переменных в параллельных асинхронных задачах.
Следующий пример составляет три контекста, каждый из которых имеет собственное значение для наименования этого значения. Функция greet() позже может использовать значение имени внутри каждого контекста:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import contextvars name = contextvars.ContextVar("name") contexts = list() def greet(): print(f"Hello {name.get()}") # Создание контекстов и установка наименования контекстной переменной for first_name in ["Steve", "Dina", "Harry"]: ctx = contextvars.copy_context() ctx.run(name.set, first_name) contexts.append(ctx) # Запуск функции <span lang="en-US">greet</span> в каждом контексте for ctx in reversed(contexts): ctx.run(greet) |
Выполняем данный код:
1 2 3 4 |
$ python3.7 context_demo.py Hello Harry Hello Dina Hello Steve |
Импорт файлов данных с “importlib.resources“
Одна из проблем при упаковке проекта Python – это выбор того, что делать с проектными ресурсами, такими как файлы данных проекта. Есть несколько привычных вариантов действий:
- Мучатся с путем к файлу данных;
- Внесение файла данных в пакет и искать его при помощи__file__.;
- Использовать setuptools.pkg_resources для получения ресурса файла данных.
Каждый из этих путей имеет свои недостатки. Первый вариант не является портативным. Использование __file__ — более портативно, но если проект Python установлен, можно оказаться в ситуации с zip архивом без атрибута __file__. Третий вариант решает эту проблему, но он удручающие медленный.
Лучшее решение – это новый модуль importlib.resources в стандартной библиотеке. Он использует существующий функционал импорта Python, чтобы импортировать, в том числе, и файлы данных. Представим, что у вас есть ресурс внутри пакета Python, вот таким образом:
1 2 3 4 |
data/ │ ├── alice_in_wonderland.txt └── __init__.py |
Обратите внимание на то, что данные должны быть пакетом Python. Таким образом, директория должна хранить файл __init__.py (который может быть пустым). После этого, вы можете читать файл alice_in_wonderland.txt, вот так:
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> 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() доступна для открытия файлов в бинарном режиме. В раннем примере «плагины как атрибуты модулей», мы использовали 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:
1 2 3 4 5 6 |
$ 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 |
В кумулятивной колонке показано кумулятивное время импорт (в микросекундах). В данном примере, импортирование плагинов заняло примерно 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 еще какое-то время.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»