# 11. Краткий обзор стандартной библиотеки — часть II

Вторая часть обзора охватывает более продвинутые модули, ориентированные на профессиональную разработку. Эти модули редко встречаются в небольших скриптах.

## 11.1. Форматирование вывода

Модуль `reprlib` предоставляет версию функции `repr()`, настроенную для сокращенного отображения больших или глубоко вложенных контейнеров:

    >>> import reprlib
    >>> reprlib.repr(set('суперкалифрагилистикэкспиалидошес'))
    "{'а', 'г', 'д', 'е', 'и', 'к', ...}"

Модуль `pprint` предлагает более тонкий контроль над печатью встроенных и пользовательских объектов в виде, удобном для чтения интерпретатором. Когда результат занимает более одной строки, «красивый принтер» добавляет переносы строк и отступы, делая структуру данных более наглядной:

    >>> import pprint
    >>> t = [[[['чёрный', 'бирюзовый'], 'белый', ['зелёный', 'красный']], [['пурпурный',
    ...     'жёлтый'], 'синий']]]
    ...
    >>> pprint.pprint(t, width=30)
    [[[['чёрный', 'бирюзовый'],
       'белый',
       ['зелёный', 'красный']],
      [['пурпурный', 'жёлтый'],
       'синий']]]

Модуль `textwrap` форматирует абзацы текста, чтобы они помещались в заданную ширину экрана:

    >>> import textwrap
    >>> doc = """Метод wrap() работает почти так же, как fill(),
    ... только возвращает список строк вместо одной длинной
    ... строки с переводами строки между частями текста."""
    ...
    >>> print(textwrap.fill(doc, width=40))
    Метод wrap() работает почти так же, как
    fill(), только возвращает список строк
    вместо одной длинной строки с переводами
    строки между частями текста.

Модуль `locale` предоставляет доступ к базе данных региональных форматов, специфичных для определённой культуры. У функции форматирования этого модуля есть специальный параметр для добавления разделителей групп в числах:

    >>> import locale
    >>> locale.setlocale(locale.LC_ALL, 'ru_RU.UTF-8')
    'ru_RU.UTF-8'
    >>> conv = locale.localeconv()          # получить таблицу настроек
    >>> x = 1234567.8
    >>> locale.format_string("%d", x, grouping=True)
    '1\xa0234\xa0567'
    >>> locale.format_string("%.*f %s", (conv['frac_digits'],
    ...                      x, conv['currency_symbol']), grouping=True)
    '1\xa0234\xa0567,80\xa0₽'

## 11.2. Шаблоны

Модуль `string` включает в себя универсальный класс `Template` с упрощенным синтаксисом, подходящим для редактирования конечными пользователями. Это позволяет пользователям настраивать свои приложения, без изменения кода программы.

Формат использует имена заполнителей, начинающихся с `$` и представляющие допустимые идентификаторы Python (буквы, цифры и подчёркивания). Окружение заполнителя фигурными скобками позволяет использовать алфавитно-цифровые символы сразу после него, без дополнительных пробелов. Написание `$$` создает одиночный экранированный `$`:

    >>> from string import Template
    >>> t = Template('${village}цы шлют $$10 в $cause.')
    >>> t.substitute(village='Ноттингем', cause='фонд канавы')
    'Ноттингемцы шлют $10 в фонд канавы.'

Метод `substitute()` выбрасывает исключение `KeyError`, когда заполнитель не предоставлен в словаре или именованном аргументе. Для приложений вроде массовой рассылки, где предоставленные пользователем данные могут быть неполными, метод `safe_substitute()` может быть более подходящим — он оставит заполнители без изменений, если данные отсутствуют:

    >>> t = Template('$item возвращается $owner.')
    >>> d = dict(item='Незагруженная ласточка')
    >>> t.substitute(d)
    Traceback (most recent call last):
      ...
    KeyError: 'owner'
    >>> t.safe_substitute(d)
    'Незагруженная ласточка возвращается $owner.'

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

    >>> import time, os.path
    >>> photofiles = ['img_1074.jpg', 'img_1076.jpg', 'img_1077.jpg']
    >>> class BatchRename(Template):
    ...     delimiter = '%'
    ...
    >>> fmt = input('Введите стиль переименования (%d-дата %n-номер %f-формат):  ')
    Введите стиль переименования (%d-дата %n-номер %f-формат):  Эшли_%n%f

    >>> t = BatchRename(fmt)
    >>> date = time.strftime('%d%b%y')
    >>> for i, filename in enumerate(photofiles):
    ...     base, ext = os.path.splitext(filename)
    ...     newname = t.substitute(d=date, n=i, f=ext)
    ...     print('{0} --> {1}'.format(filename, newname))

    img_1074.jpg --> Эшли_0.jpg
    img_1076.jpg --> Эшли_1.jpg
    img_1077.jpg --> Эшли_2.jpg

Ещё одно применение для шаблонов — отделение логики программы от деталей множества выходных форматов. Это позволяет подставлять собственные шаблоны для XML-файлов, текстовых отчётов и HTML-страниц.

## 11.3. Работа с записями бинарных данных

Модуль `struct` предоставляет функции `pack()` и `unpack()` для работы с записями переменной длины в бинарном формате. Следующий пример показывает, как пройтись по заголовкам файлов в ZIP-архиве без использования модуля `zipfile`. Коды упаковки `"H"` и `"I"` обозначают двухбайтовые и четырехбайтовые беззнаковые числа соответственно. Символ `"<"` указывает, что они имеют стандартный размер, а порядок байтов — little-endian:

    import struct

    with open('myfile.zip', 'rb') as f:
        data = f.read()

    start = 0
    for i in range(3):                      # показать заголовки первых трёх файлов
        start += 14
        fields = struct.unpack('<IIIHH', data[start:start+16])
        crc32, comp_size, uncomp_size, filenamesize, extra_size = fields

        start += 16
        filename = data[start:start+filenamesize]
        start += filenamesize
        extra = data[start:start+extra_size]
        print(filename, hex(crc32), comp_size, uncomp_size)

        start += extra_size + comp_size     # перейти к следующему заголовку

## 11.4. Многопоточность

Потоки — это техника разделения задач, которые не зависят друг от друга по порядку выполнения. Потоки можно использовать для повышения отзывчивости приложений, которые принимают пользовательский ввод, пока другие задачи выполняются в фоновом режиме. Похожий сценарий — выполнение ввода/вывода параллельно с вычислениями в другом потоке.

Следующий код показывает, как модуль высокого уровня `threading` может запускать задачи в фоновом режиме, в то время как основная программа продолжает работать:

    import threading, zipfile

    class AsyncZip(threading.Thread):
        def __init__(self, infile, outfile):
            super().__init__()
            self.infile = infile
            self.outfile = outfile

        def run(self):
            with zipfile.ZipFile(self.outfile, 'w', zipfile.ZIP_DEFLATED) as f:
                f.write(self.infile)
            print('Архивирование в фоне завершено:', self.infile)

    background = AsyncZip('mydata.txt', 'myarchive.zip')
    background.start()
    print('Основная программа продолжает выполняться на переднем плане.')

    background.join()    # Ждём, пока фоновая задача завершится
    print('Основная программа дождалась окончания фоновой задачи.')

Основная сложность многопоточных приложений — координация потоков, которые совместно используют данные или другие ресурсы. С этой целью модуль threading предоставляет ряд примитивов синхронизации, включая блокировки, события, условные переменные и семафоры.

Хотя эти инструменты эффективны, незначительные ошибки проектирования могут привести к проблемам, которые трудно воспроизвести. Таким образом, предпочтительный подход к координации задач — сосредоточить весь доступ к ресурсу в одном потоке, а затем использовать модуль `queue`, чтобы передавать этому потоку запросы от других. Приложения, использующие объекты `Queue` для межпоточного взаимодействия и координации, легче проектируются, более читабельны и надежны.

## 11.5. Логирование

Модуль `logging` предлагает полнофункциональную и гибкую систему журналирования. В самом простом случае сообщения журнала отправляются в файл или в `sys.stderr`:

    import logging
    logging.debug('Отладочная информация')
    logging.info('Информационное сообщение')
    logging.warning('Предупреждение: файл конфигурации %s не найден', 'server.conf')
    logging.error('Произошла ошибка')
    logging.critical('Критическая ошибка — завершение работы')

Это приводит к следующему выводу:

    WARNING:root:Предупреждение: файл конфигурации server.conf не найден
    ERROR:root:Произошла ошибка
    CRITICAL:root:Критическая ошибка — завершение работы

По умолчанию информационные и отладочные сообщения подавляются, а вывод отправляется в стандартный поток ошибок. Другие варианты вывода включают отправку сообщений через электронную почту, датаграммамы, сокеты или на HTTP-сервер. Новые фильтры могут выбирать разную маршрутизацию в зависимости от приоритета сообщения: `DEBUG`, `INFO`, `WARNING`, `ERROR` и `CRITICAL`.

Систему логирования можно настраивать непосредственно из Python или загружать из конфигурационного файла, редактируемого пользователем, для её конфигурации без изменения приложения.

## 11.6. Слабые ссылки

Python осуществляет автоматическое управление памятью (подсчёт ссылок для большинства объектов и сбор мусора для устранения циклов). Память освобождается вскоре после удаления последней ссылки на неё.

Этот подход отлично работает для большинства приложений, но иногда возникает необходимость отслеживать объекты только до тех пор, пока они используются чем-то ещё. К сожалению, само отслеживание создаёт ссылку, которая делает их постоянными. Модуль `weakref` предоставляет инструменты для отслеживания объектов без создания ссылки. Когда объект больше не нужен, он автоматически удаляется из таблицы слабых ссылок, и для объектов слабых ссылок запускается обратный вызов. К типичным применениям этого относится кэширование объектов, создание которых является затратным:

    >>> import weakref, gc
    >>> class A:
    ...     def __init__(self, value):
    ...         self.value = value
    ...     def __repr__(self):
    ...         return str(self.value)
    ...
    >>> a = A(10)                   # create a reference
    >>> d = weakref.WeakValueDictionary()
    >>> d['primary'] = a            # does not create a reference
    >>> d['primary']                # fetch the object if it is still alive
    10
    >>> del a                       # remove the one reference
    >>> gc.collect()                # run garbage collection right away
    0
    >>> d['primary']                # entry was automatically removed
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        d['primary']                # entry was automatically removed
      File "C:/python313/lib/weakref.py", line 46, in __getitem__
        o = self.data[key]()
    KeyError: 'primary'

## 11.7. Инструменты для работы со списками

Многие потребности в структурах данных можно удовлетворить с помощью встроенного типа списка. Однако иногда возникает необходимость в альтернативных реализациях с другим компромиссом в производительности.

Модуль `array` предоставляет объект `array`, который похож на список, но хранит только однородные данные и делает это более компактно. В следующем примере показан массив чисел, хранящийся в виде двухбайтовых беззнаковых двоичных чисел (код типа `"H"`), вместо обычных 16 байтов на элемент для стандартных списков из объектов int в Python:

    >>> from array import array
    >>> a = array('H', [4000, 10, 700, 22222])
    >>> sum(a)
    26932
    >>> a[1:3]
    array('H', [10, 700])

Модуль `collections` предоставляет объект `deque`, который похож на список с более быстрым добавлением и извлечением данных с левой стороны, но более медленным поиском в середине. Эти объекты хорошо подходят для реализации очередей и поиска по дереву в ширину:

    >>> from collections import deque
    >>> d = deque(["задача1", "задача2", "задача3"])
    >>> d.append("задача4")
    >>> print("Обработка", d.popleft())
    Обработка задача1

    unsearched = deque([starting_node])
    def breadth_first_search(unsearched):
        node = unsearched.popleft()
        for m in gen_moves(node):
            if is_goal(m):
                return m
            unsearched.append(m)

Помимо альтернативных реализаций списка, стандартная библиотека также предлагает другие инструменты, например модуль `bisect` с функциями для работы с отсортированными списками:

    >>> import bisect
    >>> scores = [(100, 'perl'), (200, 'tcl'), (400, 'lua'), (500, 'python')]
    >>> bisect.insort(scores, (300, 'ruby'))
    >>> scores
    [(100, 'perl'), (200, 'tcl'), (300, 'ruby'), (400, 'lua'), (500, 'python')]

Модуль `heapq` предоставляет функции для реализации кучи на основе обычных списков. Запись с наименьшим значением всегда сохраняется в нулевой позиции. Это полезно для приложений, которые неоднократно обращаются к наименьшему элементу, но не хотят выполнять полную сортировку списка:

    >>> from heapq import heapify, heappop, heappush
    >>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
    >>> heapify(data)                      # переупорядочить список в кучу
    >>> heappush(data, -5)                 # добавить новый элемент
    >>> [heappop(data) for i in range(3)]  # получить три наименьших элемента
    [-5, 0, 1]

## 11.8. Десятичная арифметика с плавающей точкой

Модуль `decimal` предлагает тип данных `Decimal` для десятичной арифметики с плавающей точкой. По сравнению со встроенным типом `float`, реализующим двоичные числа с плавающей точкой, этот класс особенно полезен для

- финансовых приложений и других случаев, требующих точного десятичного представления,

- контроля точности,

- контроля округления для соответствия законодательным или нормативным требованиям,

- отслеживания значащих десятичных разрядов или

- приложений, в которых пользователь ожидает, что результаты совпадут с расчётами, выполненными вручную.

Например, расчёт налога в размере 5% от счёта за телефон на 70 центов даёт разные результаты в десятичном и двоичном представлении чисел с плавающей точкой. Разница становится значимой, если результаты округлить до цента:

    >>> from decimal import *
    >>> round(Decimal('0.70') * Decimal('1.05'), 2)
    Decimal('0.74')
    >>> round(.70 * 1.05, 2)
    0.73

Результат `Decimal` сохраняет конечный ноль, автоматически выводя точность до четырёх значащих цифр в дробной части на основе множителей с двумя знаками в дробной части. Класс Decimal воспроизводит математику так, как она выполняется вручную, и позволяет избежать проблем, которые могут возникнуть, когда двоичные числа с плавающей точкой не могут точно представить десятичные величины.

Точное представление позволяет классу `Decimal` выполнять вычисления по модулю и проверки равенства, которые непригодны для двоичных чисел с плавающей точкой:

    >>> Decimal('1.00') % Decimal('.10')
    Decimal('0.00')
    >>> 1.00 % 0.10
    0.09999999999999995

    >>> sum([Decimal('0.1')]*10) == Decimal('1.0')
    True
    >>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0
    False

Модуль `decimal` обеспечивает арифметические операции с необходимой точностью:

    >>> getcontext().prec = 36
    >>> Decimal(1) / Decimal(7)
    Decimal('0.142857142857142857142857142857142857')