[Перевод] Устаревшие Python-библиотеки, с которыми пора попрощаться
В 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] {:1} ERROR - Some serious error occurred.
logging.warning('Some warning.')
# [12:52:35] {: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.
О, а приходите к нам работать?