В Python, с каждым релизом, добавляют новые модули, появляются новые и улучшенные способы решения различных задач. Все мы привыкли пользоваться старыми добрыми Python-библиотеками, привыкли к определённым способам работы. Но пришло время обновиться, время воспользоваться новыми и улучшенными модулями и их возможностями.

Pathlib
Модуль pathlib — это, определённо, одно из крупнейших недавних дополнений стандартной библиотеки Python. Этот модуль стал частью стандартной библиотеки начиная с Python 3.4. Правда, многие всё ещё пользуются модулем os для работы с файловой системой.
Но модуль pathlib, всё же, во многом лучше старого os.path. Так, модуль os представляет пути в файловой системе в виде обычных строк, а в pathlib используется объектно-ориентированный стиль. Благодаря этому повышается читабельность кода и удобство его написания:
from pathlib import Path import os.path # Старый код с плохой читабельностью two_dirs_up = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Новый, читабельный код two_dirs_up = Path(__file__).resolve().parent.parent
Тот факт, что пути рассматриваются как объекты, а не как строки, делает, кроме прочего, возможным однократное создание объекта и последующее обращение к его атрибутам или выполнение операций с ними:
readme = Path("README.md").resolve() print(f"Absolute path: {readme.absolute()}") # Absolute path: /home/martin/some/path/README.md print(f"File name: {readme.name}") # File name: README.md print(f"Path root: {readme.root}") # Path root: / print(f"Parent directory: {readme.parent}") # Parent directory: /home/martin/some/path print(f"File extension: {readme.suffix}") # File extension: .md print(f"Is it absolute: {readme.is_absolute()}") # Is it absolute: True
Одна из моих любимых возможностей pathlib, которую я хочу особо отметить, это — допустимость применения оператора / (он выглядит как математический оператор «деление») для соединения путей:
# Операторы: etc = Path('/etc') joined = etc / "cron.d" / "anacron" print(f"Exists? - {joined.exists()}") # Exists? - True
Это весьма упрощает работу с путями. Эта возможность — ну просто вишенка на «торте» pathlib.
Учитывая это — важно отметить, что модуль pathlib — это замена лишь для os.path, а не для всего модуля os. В pathlib, правда, включён и функционал из модуля glob. Поэтому, если вы привыкли пользоваться os.path в комбинации с glob.glob, это значит, что, перейдя на pathlib, вы можете забыть об их существовании.
В вышеприведённых примерах продемонстрированы некоторые удобные приёмы работы с путями и с атрибутами объекта, представляющего путь. Но в pathlib имеются ещё и методы, привычные для тех, кто работал с os.path. Например:
print(f"Working directory: {Path.cwd()}") # то же, что os.getcwd() # Working directory: /home/martin/some/path Path.mkdir(Path.cwd() / "new_dir", exist_ok=True) # то же, что os.makedirs() print(Path("README.md").resolve()) # то же, что os.path.abspath() # /home/martin/some/path/README.md print(Path.home()) # то же, что os.path.expanduser() # /home/martin
Полные сведения о соответствии функций os.path и новых функций из pathlib имеются в документации.
Больше примеров, демонстрирующих преимущества pathlib, можно найти в этой хорошей статье.
Secrets
Если продолжить разговор о модуле os, то ещё одна его часть, которую стоит отправить на покой — это os.urandom. Вместо неё лучше использовать новый модуль secrets, имеющийся в нашем распоряжении начиная с Python 3.6:
# Старый подход: import os length = 64 value = os.urandom(length) print(f"Bytes: {value}") # Bytes: b'\xfa\xf3...\xf2\x1b\xf5\xb6' print(f"Hex: {value.hex()}") # Hex: faf3cc656370e31a938e7...33d9b023c3c24f1bf5 # Новый подход: import secrets value = secrets.token_bytes(length) print(f"Bytes: {value}") # Bytes: b'U\xe9n\x87...\x85>\x04j:\xb0' value = secrets.token_hex(length) print(f"Hex: {value}") # Hex: fb5dd85e7d73f7a08b8e3...4fd9f95beb08d77391
Тут, на самом деле, без проблем можно использовать и модуль os.urandom. Но причина появления модуля secrets заключается в том, что программисты использовали модуль random для генерирования паролей и прочего подобного. И это — несмотря на то, что модуль random не выдаёт криптографически безопасные токены.
Модуль random, в соответствии с документацией, не следует использовать для целей, связанных с безопасностью. Надо применять либо secrets, либо os.urandom. Но предпочтение, определённо, стоит отдать secrets, учитывая то, что этот модуль новее, и то, что он включает в себя некоторые утилиты/удобные методы для работы с шестнадцатеричными токенами, а так же — с временными URL-адресами, содержащими маркер безопасности.
Zoneinfo
До Python 3.9 не существовало встроенного в стандартную библиотеку модуля для преобразований значений даты и времени, связанных с часовыми поясами. Поэтому все пользовались модулем pytz. Но теперь в стандартной библиотеке имеется модуль zoneinfo. А значит — пришло время переключиться на него!
from datetime import datetime import pytz # pip install pytz dt = datetime(2022, 6, 4) nyc = pytz.timezone("America/New_York") localized = nyc.localize(dt) print(f"Datetime: {localized}, Timezone: {localized.tzname()}, TZ Info: {localized.tzinfo}") # По-новому: from zoneinfo import ZoneInfo nyc = ZoneInfo("America/New_York") localized = datetime(2022, 6, 4, tzinfo=nyc) print(f"Datetime: {localized}, Timezone: {localized.tzname()}, TZ Info: {localized.tzinfo}") # Datetime: 2022-06-04 00:00:00-04:00, Timezone: EDT, TZ Info: America/New_York
Модуль datetime делегирует все манипуляции с часовыми поясами абстрактному базовому классу datetime.tzinfo. Этот абстрактный базовый класс нуждается в конкретной реализации. До выхода этого модуля такую реализацию, по всей вероятности, брали из pytz. А теперь, когда в стандартной библиотеке есть zoneinfo, этот модуль можно использовать вместо pytz.
У использования zoneinfo, правда, есть один нюанс: модуль предполагает, что в системе имеются сведения о часовых поясах. В UNIX-подобных системах это так. Если же в вашей системе таких данных нет — тогда вам понадобится пакет tzdata. Это — библиотека, поддержкой которой занимаются основные разработчики CPython. В ней имеется база данных часовых поясов IANA.
Dataclasses
Важным дополнением Python 3.7 стал пакет dataclasses (классы данных), являющийся заменой namedtuple (именованных кортежей).
Возможно, у вас появится вопрос о том, зачем менять на что-то namedtuple. Существует несколько причин перехода на dataclasses:
Поддерживается мутабельность.
По умолчанию предоставляются «магические» методы
repr,eq,init,hash.Можно указывать значения по умолчанию.
Поддерживается наследование.
Кроме того, классы данных поддерживают (начиная с Python 3.10) атрибуты frozen и slots, что делает их возможности аналогичными возможностям именованных кортежей.
Переход на dataclasses, на самом деле, не должен быть особенно сложным, так как для этого достаточно поменять определения классов:
# Старый подход: # from collections import namedtuple from typing import NamedTuple import sys User = NamedTuple("User", [("name", str), ("surname", str), ("password", bytes)]) u = User("John", "Doe", b'tfeL+uD...\xd2') print(f"Size: {sys.getsizeof(u)}") # Size: 64 # Новый подход: from dataclasses import dataclass @dataclass() class User: name: str surname: str password: bytes u = User("John", "Doe", b'tfeL+uD...\xd2') print(u) # User(name='John', surname='Doe', password=b'tfeL+uD...\xd2') print(f"Size: {sys.getsizeof(u)}, {sys.getsizeof(u) + sys.getsizeof(vars(u))}") # Size: 48, 152
В этом коде, кроме прочего, мы исследуем размеры сущностей. Это — одно из самых больших различий между namedtuple и dataclasses. Как видите, размер именованного кортежа гораздо меньше. Это так из-за того, что классы данных, для представления атрибутов, используют dict.
Если сравнить скорость работы namedtuple и dataclasses, то окажется, что скорость доступа к атрибутам класса данных будет практически такой же, как и при работе с аналогичным именованным кортежем. Она может отличаться настолько незначительно, что на это можно закрыть глаза, но лишь в том случае, если не планируется создавать миллионы экземпляров объектов:
import timeit setup = ''' from typing import NamedTuple User = NamedTuple("User", [("name", str), ("surname", str), ("password", bytes)]) u = User("John", "Doe", b'') ''' print(f"Access speed: {min(timeit.repeat('u.name', setup=setup, number=10000000))}") # Access speed: 0.16838401100540068 setup = ''' from dataclasses import dataclass @dataclass(slots=True) class User: name: str surname: str password: bytes u = User("John", "Doe", b'') ''' print(f"Access speed: {min(timeit.repeat('u.name', setup=setup, number=10000000))}") # Access speed: 0.17728697300481144
Если вышесказанное убедило вас перейти на классы данных, но вы вынуждены применять Python 3.6 или более раннюю версию языка, можете воспользоваться соответствующим бэкпортом.
И наоборот — если переходить на классы данных вы не хотите, если по какой-то причине вам действительно нужны именованные кортежи, тогда вам стоит, как минимум, пользоваться NamedTuple из модуля typing, а не из модуля collections:
# Плохо: from collections import namedtuple Point = namedtuple("Point", ["x", "y"]) # Лучше: from typing import NamedTuple class Point(NamedTuple): x: float y: float
И, наконец, если вы не пользуетесь ни namedtyple, ни dataclasses, то вам, возможно, стоит взглянуть на проект pydantic.
Качественное логирование
Тут речь пойдёт не о некоем недавнем дополнении стандартной библиотеки. Разговоры о логировании не новы, но нелишним будет снова поднять эту тему: используйте адекватные способы логирования вместо инструкций print. Если вы занимаетесь локальной отладкой — вполне можно пользоваться print. Но для чего-то, уходящего в продакшн, работающего самостоятельно, без вмешательства пользователя, совершенно необходимо нормальное логирование.
К тому же, такое логирование организовать очень просто — достаточно воспользоваться модулем logging и выполнить некоторые несложные настройки:
import logging logging.basicConfig( filename='application.log', level=logging.WARNING, format='[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s', datefmt='%H:%M:%S' ) logging.error("Some serious error occurred.") # [12:52:35] {<stdin>:1} ERROR - Some serious error occurred. logging.warning('Some warning.') # [12:52:35] {<stdin>:1} WARNING - Some warning.
Даже та простейшая конфигурация логирования, что показана выше, даст, в сравнении с print, серьёзное повышение удобства отладки. Кроме того, библиотеку logging можно настраивать и дальше, сделав так, чтобы логи отправлялись бы в разные места, задавая уровни логирования, настраивая автоматическую ротацию логов и так далее. Подробности о том, как всё это настроить, смотрите в одной из моих предыдущих статей, посвящённой отладке в Python.
F-строки
В Python имеется достаточно много способов форматирования строк. Сюда входит форматирование в стиле C, f-строки, шаблонные строки, функция .format. Среди этих способов стоит отметить f-строки (f-strings), форматированные строковые литералы. Это — нечто совершенно замечательное. Они, в сравнении с другими способами форматирования строк, удобнее в написании, читабельнее, а ещё — быстрее всех остальных.
В результате я полагаю, что нет смысла что-то доказывать или объяснять, агитируя за использование f-строк. Правда, есть пара ситуаций, когда f-строки использовать не получится.
Так, одна из ситуаций, когда нужно пользоваться форматированием с применением % — формирование сообщений для логирования:
import logging things = "something happened..." logger = logging.getLogger(__name__) logger.error("Message: %s", things) # Вычисляется в методе логирования logger.error(f"Message: {things}") # Вычисляется немедленно
В этом примере, если воспользоваться f-строками, выражение будет вычислено немедленно. А применение стиля форматирования C позволяет отложить замену шаблона на реальные данные до того момента, когда это будет действительно нужно. Это важно для группировки сообщений, когда все сообщения с одним и тем же шаблоном можно записать как одно сообщение. А с применением f-строк так не получится, так как шаблон заполняется данными до передачи системе логирования.
Кроме того, есть вещи, которые f-строки просто не умеют. Например — формирование шаблона во время выполнения программы, то есть — динамическое форматирование. Именно поэтому использование f-строк называют форматированием с помощью строковых литералов:
# Динамическое формирование и шаблона, и его параметров def func(tpl: str, param1: str, param2: str) -> str: return tpl.format(param=param1, param2=param2) some_template = "First template: {param1}, {param2}" another_template = "Other template: {param1} and {param2}" print(func(some_template, "Hello", "World")) print(func(another_template, "Hello", "Python")) # Динамическое переиспользование одного и того же шаблона с разными параметрами inputs = ["Hello", "World", "!"] template = "Here's some dynamic value: {value}" for value in inputs: print(template.format(value=value))
В итоге можно порекомендовать использовать f-строки везде, где это возможно, так как они читабельнее и производительнее других способов форматирования текста в Python. Но стоит помнить о том, что в некоторых случаях лучше (или необходимо) пользоваться другими механизмами.
Читайте «F-строки в Python мощнее, чем можно подумать» у нас в блоге
Tomllib
TOML — это широко используемый формат конфигурационных файлов, который особенно важен при работе с Python-инструментами и, в целом, в экосистеме Python. Всё дело в том, что он используется в конфигурационных файлах pyproject.toml. До настоящего времени для управления TOML-файлами необходимо было использовать внешние библиотеки. Но, начиная с Python 3.11, в нашем распоряжении окажется встроенная библиотека, названная tomllib, основанная на пакете tomli.
Как только вы перейдёте на Python 3.11, у вас должна появиться привычка использовать import tomllib вместо import tomli. В результате вам придётся заботиться о меньшем количестве зависимостей вашего проекта!
# import tomli as tomllib import tomllib with open("pyproject.toml", "rb") as f: config = tomllib.load(f) print(config) # {'project': {'authors': [{'email': 'contact@martinheinz.dev', # 'name': 'Martin Heinz'}], # 'dependencies': ['flask', 'requests'], # 'description': 'Example Package', # 'name': 'some-app', # 'version': '0.1.0'}} toml_string = """ [project] name = "another-app" description = "Example Package" version = "0.1.1" """ config = tomllib.loads(toml_string) print(config) # {'project': {'name': 'another-app', 'description': 'Example Package', 'version': '0.1.1'}}
Setuptools
Наш последний раздел посвящён, в основном, уведомлению о том, что пакет distutils признан устаревшим:
Так как пакет distutils признан устаревшим, любое использование функций или объектов из этого пакета не приветствуется. Пакет setuptools ориентирован на замену или вывод из обращения устаревших механизмов.
Пришло время попрощаться с пакетом distutils и перейти на setuptools. Документация по setuptools содержит руководство о том, как перейти с distutils на setuptools. Кроме того, в PEP 632 можно найти рекомендации по миграции с тех частей distutils, которые не перекрывает функционал setuptools.
Итоги
Каждый новый релиз Python несёт в себе новые возможности. Поэтому рекомендую, заглядывая в примечания к выпуску (release notes) Python, обращать внимание на разделы «Новые модули» (New Modules), «Устаревшие модули» (Deprecated modules) и «Удалённые модули» (Removed modules). Это — хороший способ оставаться в курсе крупных изменений стандартной библиотеки Python. При таком подходе вы сможете постоянно включать в свои проекты новые возможности и следовать рекомендациям по разработке.
Возможно, вам кажется, что внесение всех этих изменений и обновлений в проекты потребует большой работы. Но, на самом деле, этот процесс можно облегчить, обработав проекты с помощью pyupgrade. Это позволит, там, где это возможно, автоматически обновить код до последней версии Python.
О, а приходите к нам работать? ? ?
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
