Устаревшие Python-библиотеки, с которыми пора попрощаться +28


В 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:

  • Поддерживается мутабельность.

  • По умолчанию предоставляются «магические» методы repreqinithash.

  • Можно указывать значения по умолчанию.

  • Поддерживается наследование.

Кроме того, классы данных поддерживают (начиная с 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 разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде.




Комментарии (22):

  1. dyadyaSerezha
    /#24608648 / +29

    1. Название статьи (устаревшие...) противоречит названиям разделов (новые...).

    2. Не понял преимуществ dataclasses.

    3. Там же, в выводе времени (timeit) лучше писать не "Access speed", а "Access time", чтобы не путать читающих логи. И дополнить после собственно времени, в чем измерялось время.

    • ti_zh_vrach
      /#24609562

      Добавлю про pathlib.

      Вставил PurePath.joinpath() в лоб вместо os.path.join(). Словил AttributeError: 'str' object has no attribute '_make_child'.

      А вот os.path.join() принял <class 'pathlib.WindowsPath'> и привёл путь к str.

      Я не настоящий сварщик, но мне это странно.

      • egorro_13
        /#24609702 / +1

        os.path.join('/mnt', 'e')
        PurePath('/mnt').joinpath('e')
        pathlib.Path('/mnt') / 'e'

        3 варианта одного и того же объединения

        • ti_zh_vrach
          /#24609778 / -1

          Кажется, это не совсем одно и то же:

          print(type(os.path.join('/mnt', 'e')))
          >>> <class 'str'>
          
          print(type(PurePath('/mnt').joinpath('e')))
          >>> <class 'pathlib.PureWindowsPath'>
          
          print(type(Path('/mnt') / 'e'))
          >>> <class 'pathlib.WindowsPath'>
          

          Мне именно в этом видится корень проблемы.
          Например, во втором случае можно словить AttributeError: 'PureWindowsPath' object has no attribute 'endswith', если .endswith() используется где-то в недрах чужой либы. Что у меня и произошло после написания комментария.

          • egorro_13
            /#24609858 / +1

            Кажется, это не совсем одно и то же:

            В смысле, что 3 способа получения пути к одной и той же папке через объединение двух путей. Изначально же речь шла о некорректном объединении (вызове метода у класса, а не объекта) и соответствующей ошибке:

            Вставил PurePath.joinpath() в лоб вместо os.path.join(). Словил AttributeError: 'str' object has no attribute '_make_child'.

            А для чужих либ можно использовать str(path), если они напрямую не поддерживают Path по каким-то причинам.

        • SomeAnonimCoder
          /#24611236 / +1

          Есть еще дико удобный glob, с unix-style путями и wildcards

          import glob
          glob.glob('mnt/e') # папка /mnt/e
          glob.glob('mnt/e/*') # всe файлы в mnt/e

          • egorro_13
            /#24614432

            Аналогично можно использовать и все тот же Path)

            mnt = pathlib.Path('/mnt')
            mnt.glob('e') # папка /mnt/e
            mnt.glob('*') # все файлы в /mnt/
            mnt.rglob('*') # все файлы в /mnt/ и подпапках

          • vectorplus
            /#24615196

            А как он с виндой работает? Спрашиваю для друга.

  2. Z55
    /#24609568 / +7

    Не понял, а как согласуется название статьи и раздел про logging и f-строки?

  3. vectorplus
    /#24611426 / +1

    Панды устаревшая библиотека

    • abagnale
      /#24613002 / +1

      Это какая? Не pandas же?

      • vectorplus
        /#24613084 / +1

        Именно. Wes McKinney, создатель панд, еще в 2013 году написал пост 10 Things Why I Hate Pandas. Вот, в 2017 он опять распедалил по этому поводу. Я сам нежно относился к этой библиотеке, пока опытные товарищи не сказали, что панды использовать не стоит ни в коем случае. Я удивился и начал копать, оказалось, это вполне известная тема. Эта библиотека хороша на инфоцыганских курсах, когда надо покрутить крошечные датасеты, но в серьезной ежедневной работе ей не место.

        • abagnale
          /#24613108 / +1

          Знаю больше одного и даже больше двух астрономов, которые используют pandas каждый день для работы со своими астрономическими данными. Не только не жалуются, но и другим рекомендуют, я собственно от них о ней и узнал.

          А что тогда предлагается использовать для работы с "табличными" данными, если не pandas? В той статье по вашей ссылке, я так понял, он рекомендует Apache Arrow (не доводилось работать с этой библиотекой).

          • vectorplus
            /#24613198 / +1

            Исследователи вообще R предпочитают обычно. Я про астрономию ничего не знаю, но гугл говорит, что в телескоп видно пять миллионов звезд, это смешно для дата саенс. На последнем месте работы я имел дело с примерно таким количеством транзакций в час.

            Да, Arrow все советуют.

            • abagnale
              /#24613396 / +1

              Датасеты у них и правда небольшие. Они работают с экзопланетами, и например в каталоге NASA сейчас всего 3797 таких звёздных систем (с 5069 планетами), так что с такими объёмами они конечно могут продолжать пользоваться pandas.

              А я теперь попробую Apache Arrow, спасибо за ваш коментарий и ссылку.

            • Stas911
              /#24615142 / +1

              Ну, как бы не всем нужно забивать гвозди микроскопом - в жизни полно прикладных задач, когда экселя уже мало, а хадупа - много. Запустил ноутбук, быстро на пандах набросал, запустил один раз, пока кофе пьешь - оно и посчиталось.

              ЗЫ: Как ваша работа, кстати? После той увлекательной статьи продолжения так и не вышло?

              • vectorplus
                /#24615174

                Согласен! Зачем использовать левый язык, когда есть R!
                :)
                Работу я так и не нашел, всем нужен диплом, опыт никого не устраивает. У меня сейчас свой стартап, агритек. Собрались отличные ребята, делаем умный гроубокс, вчера был третий митинг с голландским акселератором. Мы друг другу нравимся, скорее всего, осенью будем в Лимбурге.

                Надо написать статью, когда мы запилим прототип. Проект называется Tom Umber, запомните это имя! )

                • Stas911
                  /#24615198 / +1

                  Ну что ж, Джека Ма, вон, даже в Мак не взяли работать. Может это и не ваше. Удачи со стартапом!

                  R я тоже как-то ковырял и даже прошел курс на Курсере (там был какой-то очень злой 4хнедельный, с дикими дедлайнами) - осилил его только со второй попытки. Вот только в моей области он редко используется, а когда не пользуешься - забывается очень быстро.

                  • vectorplus
                    /#24615218

                    R прекрасен, когда работаешь только с цифрами. Но когда нужно и скрипты писать, и то, и се, и третье, и десятое, то Питон, безусловно, язык выбора.

                    Наверное, стартапы это мой путь. Я чувствую себя как рыба в воде, питча очередного person of interest и организовывая команду. Мы уже подались на YCombinator,на эстонскую стартап визу, и мне все это ужасно нравится, не смотря на то, что я все зафейлил (ожидаемо). Дата саентист я посредственный, а с людьми общаться прям обожаю :)

                    Кстати, спасибо за референс на Амазон. Мне в итоге так и не ответили, но опыт был очень полезный. Четырнадцать Заповедей Строителя Коммунизма я запомнил и сделал выводы.

                    Джек Ма классный! :)

  4. Ximus
    /#24611698

    Здесь забыли добавить результат print()

    localized = nyc.localize(dt)

    print(f"Datetime: {localized}, Timezone: {localized.tzname()}, TZ Info: {localized.tzinfo}")

  5. Sonic_SE
    /#24618526

    С logging все переходят на loguru.