Говорят, что новый Python 3.7 стал намного быстрее и удобнее. Мы решили убедиться в этом и детально разобрали 7 важных изменений.
Стало легче создавать классы и работать с модулями, давно используемые неофициальные возможности наконец-то получили признание, а измерение времени вышло на новый уровень.
Упрощенное создание классов
В Python 3.7 появился новый модуль dataclasses и декоратор @dataclass , облегчающий создание пользовательских классов. Он автоматически добавляет специальные методы вроде __init__ , __repr__ и __eq__ .
Вот он – пример отличного шаблона класса в лучших практиках кодинга. Только вспомните, как пришлось бы создавать Country раньше. Помимо beach_per_person нужно было описать методы инициализации, вывода и целых 6 методов для сравнения. Просто посмотрите на это:
[spoiler title=’Класс Country в привычном виде’]
Новый Python берет эту тяжелую работу на себя.
Классы данных совершенно идентичны обычным. Например, от них легко можно наследовать. И используются они аналогичным образом:
Поля name , population , area и coastline устанавливаются при инициализации класса. При этом длина береговой линии – это необязательный параметр. Закрытый со всех сторон Непал, например, его не использует.
По умолчанию, классы данных можно сравнивать, а если в декораторе установлено свойство order , равное True , то еще и сортировать.
Сортировка идет поочередно по значениям полей, начиная с name . Существует возможность настроить с помощью функции field , какие конкретно поля должны в этом участвовать. Например, свойство area не учитывается ни в выводе, ни в сравнении.
Классы данных очень похожи на namedtuple. Также их создатели черпали вдохновение в проекте attrs. Больше информации о новом модуле вы можете найти в PEP 557.
Атрибуты модулей
Атрибуты в Python везде, на них основана такая базовая функциональность языка, как интроспекция, документирование кода и пространства имен.
Обратиться к атрибуту можно с помощью оператора точка thing.attribute . Для получения атрибутов, созданных в процессе выполнения программы, используется функция getattr :
Этот код выведет нечто подобное:
Если вы вызываете thing.attribute для классов, то первым делом интерпретатор проверяет, определен ли такой атрибут у thing . Если нет, то он передает управление методу thing.__getattr__(“attr”) . Это упрощенное описание, узнать подробнее вы можете здесь. Таким образом, с помощью __getattr__ можно контролировать доступ к атрибутам объекта.
До появления версии 3.7 осуществить подобное с модулями было непросто. Однако новый Python добавил в них __getattr__ и __dir__ – специальный метод для настройки вызова dir .
PEP 562 демонстрирует несколько примеров использования этих функций. В частности, с их помощью можно реализовать оповещения об устаревании или медленной загрузке. А мы напишем несложную систему плагинов для динамического добавления функций в модуль. Вам потребуется общее представление о пакетах в Python. Чтобы освежить его, загляните сюда.
Создайте папку с именем plugins , а в ней – __init__.py :
Разместите код плагинов в файлах plugin_1.py и plugin_2.py :
Теперь запустите программу:
Здесь происходит что-то удивительное. Чтобы вызвать plugins.hello_1 , мы должны были явно импортировать его в __init__.py , разве не так? Теперь нет!
Новые возможности модулей
Мы просто определяем hello_1 в любом файле пакета, и он самостоятельно регистрируется благодаря декоратору.
Есть разница, правда? Эта простая структура позволяет добавлять функциональность без привязки к остальному коду и централизованного объявления доступности.
Итак, что же делает __getattr__ внутри __init__.py ? При вызове plugins.hello_1 интерпретатор сначала проверяет, объявлена ли такая функция и, естественно, не находит ее. Тогда он обращается к методу __getattr__(“hello_1”) . У нас он выглядит вот так:
Что делает этот метод?
- Проверяет существование свойства с именем “hello_1” в словаре PLUGINS . Но на данный момент его там нет.
- Из-за неудачи переходит в секцию except и импортирует плагины.
- После этого у PLUGINS должно появиться нужное свойство, которое можно вернуть вызывающему коду.
- Если что-то пошло не так, и после импорта нужный плагин не обнаружился, функция выбросит ошибку AttributeError .
По-прежнему непонятно, каким образом плагины попадают в словарь, ведь _import_plugins просто загружает файлы, но не изменяет PLUGINS :
Вспомните, что каждый плагин декорирован с помощью @register_plugin . При импорте он и записывает свое имя в в словарь. Загрузите один файл самостоятельно, чтобы убедиться в этом:
Теперь вызовем dir и увидим, что импортируются оставшиеся плагины.
Обычно этот метод перечисляет все атрибуты полученного объекта, что выглядит примерно так:
Конечно, это довольно полезно, но хотелось бы просто увидеть доступные плагины. Новый Python позволяет настроить и это. Специальная функция __dir__ изменяет результаты вызова dir . В примере она сначала проверяет, импортированы ли все плагины, а затем выводит список их имен.
Здесь используется еще одно нововведение Python 3.7: модуль importlib.resources. С его помощью мы загрузили модули из папки plugins без обращения к __file__ или pkg_resources .
Наносекундная точность
Python 3.7 добавил несколько новых возможностей в модуль time, которые были описаны в PEP 564, например:
- clock_gettime_ns() – получение времени указанных часов;
- clock_settime_ns() – установка времени указанных часов;
- monotonic_ns() – получение времени относительных часов без отката (например, из-за летнего времени);
- perf_counter_ns() – получение значения счетчика выполнения, предназначенного для измерения коротких промежутков;
- process_time_ns() – получение суммы системного времени для конкретного процесса (без учета спящего режима);
- time_ns() – получение количества наносекунд с 01.01.1970.
Похоже, никакой новой функциональности тут нет. Каждый метод уже имеет свой аналог, лишь добавился суффикс _ns . Разница заключается в том, что эти новые функции возвращают значение с типом int в наносекундах вместо количества секунд с типом float .
Большинство приложений не заметит эту разницу. Однако в целом работа со временем становится понятнее и проще. Дело в том, что тип float по определению неточный, в отличие от int :
Это имеет значение для больших чисел, так как компьютер вынужден умещать бесконечное количество символов в ограниченном объеме битов.
Для типа float в Python по стандарту используется 53 значащих байта. Поэтому любой период времени больше 104 дней (9 квадриллионов наносекунд) нельзя точно представить в типе float . А вот int в Python не имеет ограничений, поэтому в нем можно хранить любые значения с наносекундной точностью.
Например, time.time() возвращает прошедшее с начала UNIX-эпохи время в секундах – огромное число. Новая _ns версия функции работает почти в 3 раза быстрее старой.
Модуль datetime работает только с микросекундами:
Поэтому, если вы желаете наносекундной точности, обратите внимание на проект astropy и его пакет astropy.time, который доступен в Python 3.5+.
Официальное упорядочивание словарей
Python 3.7 на официальном уровне зафиксировал соответствие порядка перебора элементов словарей порядку их добавления.
Разве это новость? – спросите вы. В Python 3.6 словари уже были упорядочены, что видно на примере:
Однако это был просто удобный побочный результат реализации, не зафиксированный в стандарте. Новый Python оформил его официально. Теперь можно быть уверенным в сохранении порядка вставки.
Признание async и await
Синтаксис async/await появился еще в Python 3.5, однако только сейчас эти слова официально стали ключевыми.
Теперь нельзя объявлять функции и переменные с такими именами.
Это не было сделано раньше в целях обеспечения обратной совместимости, но наконец время пришло.
Переменные контекста
Эти переменные могут хранить разные значения в зависимости от контекста использования, что очень похоже на потоковое локальное хранилище, где каждый поток работает с собственными данные. Это полезно для отслеживания значения в асинхронных задачах.
Создадим три контекста, каждый с собственным name :
Этот скрипт поприветствует в обратном порядке Стива, Дину и Гарри:
Новый уровень asyncio
Модуль asyncio предназначен для асинхронного выполнения кода. Он был добавлен в Python 3.4. Если вы до сих пор с ним не знакомы, обратите внимание на это руководство.
Новый Python добавил в asyncio ряд новых функций и поддержку переменных контекста. Например, asyncio.run() позволяет вызывать сопрограммы без создания событийного цикла:
В целом модуль стал работать быстрее и эффективнее.
Переход на новый Python
Конечно, вам уже не терпится попробовать все новые возможности языка. Установите Python 3.7 и смело экспериментируйте! Вы даже можете использовать разные версии интерпретатора параллельно с помощью pyenv или Anaconda.
Если вы уже задумались об обновлении работающего проекта, тщательно взвесьте это решение и протестируйте все изменения. Помните о нескольких нововведениях, которые могут сломать старый код. В их числе ключевые слова async и await .
Большинство обновок Python 3.7, например, dataclasses , имеют бэкпорты в более старых версиях языка или менее удобные аналоги. Но использование некоторых возможностей крепко привяжет ваш код к новому стандарту. Среди них наносекундный тайминг и атрибуты модулей.