[Перевод] Устаревшие Python-библиотеки, с которыми пора попрощаться

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

56863d30cbff6aa55661935486bf276c.png

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.

О, а приходите к нам работать?

© Habrahabr.ru