Говорят, что новый Python 3.7 стал намного быстрее и удобнее. Мы решили убедиться в этом и детально разобрали 7 важных изменений.
Стало легче создавать классы и работать с модулями, давно используемые неофициальные возможности наконец-то получили признание, а измерение времени вышло на новый уровень.
Упрощенное создание классов
В Python 3.7 появился новый модуль dataclasses и декоратор @dataclass
, облегчающий создание пользовательских классов. Он автоматически добавляет специальные методы вроде __init__
, __repr__
и __eq__
.
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): """Meters of coastline per person""" return (self.coastline * 1000) / self.population
Вот он – пример отличного шаблона класса в лучших практиках кодинга. Только вспомните, как пришлось бы создавать Country
раньше. Помимо beach_per_person
нужно было описать методы инициализации, вывода и целых 6 методов для сравнения. Просто посмотрите на это:
[spoiler title='Класс Country в привычном виде']
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): """Meters of coastline per person""" return (self.coastline * 1000) / self.population
[/spoiler]
Новый Python берет эту тяжелую работу на себя.
Классы данных совершенно идентичны обычным. Например, от них легко можно наследовать. И используются они аналогичным образом:
>>> 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
устанавливаются при инициализации класса. При этом длина береговой линии – это необязательный параметр. Закрытый со всех сторон Непал, например, его не использует.
По умолчанию, классы данных можно сравнивать, а если в декораторе установлено свойство order
, равное True
, то еще и сортировать.
>>> 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
. Существует возможность настроить с помощью функции field
, какие конкретно поля должны в этом участвовать. Например, свойство area
не учитывается ни в выводе, ни в сравнении.
Классы данных очень похожи на namedtuple. Также их создатели черпали вдохновение в проекте attrs. Больше информации о новом модуле вы можете найти в PEP 557.
Атрибуты модулей
Атрибуты в Python везде, на них основана такая базовая функциональность языка, как интроспекция, документирование кода и пространства имен.
Обратиться к атрибуту можно с помощью оператора точка thing.attribute
. Для получения атрибутов, созданных в процессе выполнения программы, используется функция getattr
:
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)}")
Этот код выведет нечто подобное:
A gammavariate random value: 2.8017715125270618
Если вы вызываете thing.attribute
для классов, то первым делом интерпретатор проверяет, определен ли такой атрибут у thing
. Если нет, то он передает управление методу thing.__getattr__("attr")
. Это упрощенное описание, узнать подробнее вы можете здесь. Таким образом, с помощью __getattr__
можно контролировать доступ к атрибутам объекта.
До появления версии 3.7 осуществить подобное с модулями было непросто. Однако новый Python добавил в них __getattr__
и __dir__
– специальный метод для настройки вызова dir
.
PEP 562 демонстрирует несколько примеров использования этих функций. В частности, с их помощью можно реализовать оповещения об устаревании или медленной загрузке. А мы напишем несложную систему плагинов для динамического добавления функций в модуль. Вам потребуется общее представление о пакетах в Python. Чтобы освежить его, загляните сюда.
Создайте папку с именем plugins
, а в ней – __init__.py
:
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]}")
Разместите код плагинов в файлах plugin_1.py
и plugin_2.py
:
from . import register_plugin @register_plugin def hello_1(): print("Hello from Plugin 1")
from . import register_plugin @register_plugin def hello_2(): print("Hello from Plugin 2") @register_plugin def goodbye(): print("Plugin 2 says goodbye")
Теперь запустите программу:
>>> import plugins >>> plugins.hello_1() Hello from Plugin 1 >>> dir(plugins) ['goodbye', 'hello_1', 'hello_2'] >>> plugins.goodbye() Plugin 2 says goodbye
Здесь происходит что-то удивительное. Чтобы вызвать plugins.hello_1
, мы должны были явно импортировать его в __init__.py
, разве не так? Теперь нет!
Новые возможности модулей
Мы просто определяем hello_1
в любом файле пакета, и он самостоятельно регистрируется благодаря декоратору.
Есть разница, правда? Эта простая структура позволяет добавлять функциональность без привязки к остальному коду и централизованного объявления доступности.
Итак, что же делает __getattr__
внутри __init__.py
? При вызове plugins.hello_1
интерпретатор сначала проверяет, объявлена ли такая функция и, естественно, не находит ее. Тогда он обращается к методу __getattr__("hello_1")
. У нас он выглядит вот так:
def __getattr__(name): """Возвращает плагин по имени""" 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
Что делает этот метод?
- Проверяет существование свойства с именем
"hello_1"
в словареPLUGINS
. Но на данный момент его там нет. - Из-за неудачи переходит в секцию
except
и импортирует плагины. - После этого у
PLUGINS
должно появиться нужное свойство, которое можно вернуть вызывающему коду. - Если что-то пошло не так, и после импорта нужный плагин не обнаружился, функция выбросит ошибку
AttributeError
.
По-прежнему непонятно, каким образом плагины попадают в словарь, ведь _import_plugins
просто загружает файлы, но не изменяет PLUGINS
:
def _import_plugins(): """Импортирует все ресурсы для регистрации плагинов""" for name in resources.contents(__name__): if name.endswith(".py"): import_module(f"{__name__}.{name[:-3]}")
Вспомните, что каждый плагин декорирован с помощью @register_plugin
. При импорте он и записывает свое имя в в словарь. Загрузите один файл самостоятельно, чтобы убедиться в этом:
>>> import plugins >>> plugins.PLUGINS {} >>> import plugins.plugin_1 >>> plugins.PLUGINS {'hello_1': <function hello_1 at 0x7f29d4341598>}
Теперь вызовем dir
и увидим, что импортируются оставшиеся плагины.
>>> 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>}
Обычно этот метод перечисляет все атрибуты полученного объекта, что выглядит примерно так:
>>> import plugins >>> dir(plugins) ['PLUGINS', '__builtins__', '__cached__', '__doc__', '__file__', '__getattr__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '_import_plugins', 'import_module', 'register_plugin', 'resources']
Конечно, это довольно полезно, но хотелось бы просто увидеть доступные плагины. Новый Python позволяет настроить и это. Специальная функция __dir__
изменяет результаты вызова dir
. В примере она сначала проверяет, импортированы ли все плагины, а затем выводит список их имен.
def __dir__(): """Список доступных плагинов""" _import_plugins() return list(PLUGINS.keys())
Здесь используется еще одно нововведение 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
:
>>> 0.1 + 0.1 + 0.1 0.30000000000000004 >>> 0.1 + 0.1 + 0.1 == 0.3 False
Это имеет значение для больших чисел, так как компьютер вынужден умещать бесконечное количество символов в ограниченном объеме битов.
Для типа float
в Python по стандарту используется 53 значащих байта. Поэтому любой период времени больше 104 дней (9 квадриллионов наносекунд) нельзя точно представить в типе float
. А вот int
в Python не имеет ограничений, поэтому в нем можно хранить любые значения с наносекундной точностью.
Например, time.time()
возвращает прошедшее с начала UNIX-эпохи время в секундах – огромное число. Новая _ns
версия функции работает почти в 3 раза быстрее старой.
Модуль datetime
работает только с микросекундами:
>>> 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, который доступен в Python 3.5+.
>>> 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
Официальное упорядочивание словарей
Python 3.7 на официальном уровне зафиксировал соответствие порядка перебора элементов словарей порядку их добавления.
Разве это новость? – спросите вы. В Python 3.6 словари уже были упорядочены, что видно на примере:
# Python <= 3.5 >>> {"one": 1, "two": 2, "three": 3} {'three': 3, 'one': 1, 'two': 2} # Python >= 3.6 >>> {"one": 1, "two": 2, "three": 3} {'one': 1, 'two': 2, 'three': 3}
Однако это был просто удобный побочный результат реализации, не зафиксированный в стандарте. Новый Python оформил его официально. Теперь можно быть уверенным в сохранении порядка вставки.
Признание async и await
Синтаксис async/await
появился еще в Python 3.5, однако только сейчас эти слова официально стали ключевыми.
Теперь нельзя объявлять функции и переменные с такими именами.
>>> async = 1 File "<stdin>", line 1 async = 1 ^ SyntaxError: invalid syntax >>> def await(): File "<stdin>", line 1 def await(): ^ SyntaxError: invalid syntax
Это не было сделано раньше в целях обеспечения обратной совместимости, но наконец время пришло.
Переменные контекста
Эти переменные могут хранить разные значения в зависимости от контекста использования, что очень похоже на потоковое локальное хранилище, где каждый поток работает с собственными данные. Это полезно для отслеживания значения в асинхронных задачах.
Создадим три контекста, каждый с собственным name
:
import contextvars name = contextvars.ContextVar("name") contexts = list() def greet(): print(f"Hello {name.get()}") # создание контекстов с разными значениями переменной name for first_name in ["Steve", "Dina", "Harry"]: ctx = contextvars.copy_context() ctx.run(name.set, first_name) contexts.append(ctx) # запуск функции в каждом контексте for ctx in reversed(contexts): ctx.run(greet)
Этот скрипт поприветствует в обратном порядке Стива, Дину и Гарри:
$ python3.7 context_demo.py Hello Harry Hello Dina Hello Steve
Новый уровень asyncio
Модуль asyncio предназначен для асинхронного выполнения кода. Он был добавлен в Python 3.4. Если вы до сих пор с ним не знакомы, обратите внимание на это руководство.
Новый Python добавил в asyncio ряд новых функций и поддержку переменных контекста. Например, asyncio.run() позволяет вызывать сопрограммы без создания событийного цикла:
import asyncio async def hello_world(): print("Hello World!") asyncio.run(hello_world())
В целом модуль стал работать быстрее и эффективнее.
Переход на новый Python
Конечно, вам уже не терпится попробовать все новые возможности языка. Установите Python 3.7 и смело экспериментируйте! Вы даже можете использовать разные версии интерпретатора параллельно с помощью pyenv или Anaconda.
Если вы уже задумались об обновлении работающего проекта, тщательно взвесьте это решение и протестируйте все изменения. Помните о нескольких нововведениях, которые могут сломать старый код. В их числе ключевые слова async
и await
.
Большинство обновок Python 3.7, например, dataclasses
, имеют бэкпорты в более старых версиях языка или менее удобные аналоги. Но использование некоторых возможностей крепко привяжет ваш код к новому стандарту. Среди них наносекундный тайминг и атрибуты модулей.
Здесь вы найдете несколько рекомендаций по обновлению.
Перевод статьи Cool New Features in Python 3.7.
Комментарии