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

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

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

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

>>> import reprlib
>>> reprlib.repr(set('supercalifragilisticexpialidocious'))
"{'a', 'c', 'd', 'e', 'f', 'g', ...}"

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

>>> import pprint
>>> t = [[[['black', 'cyan'], 'white', ['green', 'red']], [['magenta',
...     'yellow'], 'blue']]]
...
>>> pprint.pprint(t, width=30)
[[[['black', 'cyan'],
   'white',
   ['green', 'red']],
  [['magenta', 'yellow'],
   'blue']]]

Модуль textwrap форматирует абзацы текста под определённую ширину:

>>> import textwrap
>>> doc = """The wrap() method is just like fill() except that it returns
... a list of strings instead of one big string with newlines to separate
... the wrapped lines."""
...
>>> print(textwrap.fill(doc, width=40))
The wrap() method is just like fill()
except that it returns a list of strings
instead of one big string with newlines
to separate the wrapped lines.

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

>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'English_United States.1252')
'English_United States.1252'
>>> conv = locale.localeconv()          # получить отображения соглашений
>>> x = 1234567.8
>>> locale.format("%d", x, grouping=True)
'1,234,567'
>>> locale.format_string("%s%.*f", (conv['currency_symbol'],
...                      conv['frac_digits'], x), grouping=True)
'$1,234,567.80'

11.2. Шаблонизация

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

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

>>> from string import Template
>>> t = Template('${village}folk send $$10 to $cause.')
>>> t.substitute(village='Nottingham', cause='the ditch fund')
'Nottinghamfolk send $10 to the ditch fund.'

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

>>> t = Template('Return the $item to $owner.')
>>> d = dict(item='unladen swallow')
>>> t.substitute(d)
Traceback (most recent call last):
  ...
KeyError: 'owner'
>>> t.safe_substitute(d)
'Return the unladen swallow to $owner.'

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

>>> import time, os.path
>>> photofiles = ['img_1074.jpg', 'img_1076.jpg', 'img_1077.jpg']
>>> class BatchRename(Template):
...     delimiter = '%'
>>> fmt = input('Enter rename style (%d-date %n-seqnum %f-format):  ')
Enter rename style (%d-date %n-seqnum %f-format):  Ashley_%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 --> Ashley_0.jpg
img_1076.jpg --> Ashley_1.jpg
img_1077.jpg --> Ashley_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):                      # показать первые 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):
        threading.Thread.__init__(self)
        self.infile = infile
        self.outfile = outfile

    def run(self):
        f = zipfile.ZipFile(self.outfile, 'w', zipfile.ZIP_DEFLATED)
        f.write(self.infile)
        f.close()
        print('Готовый фоновый zip архив:', self.infile)

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

background.join()    # Дождаться завершения фоновой задачи
print('Main program waited until background was done.')

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

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

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

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

import logging
logging.debug('Debugging information')
logging.info('Informational message')
logging.warning('Warning:config file %s not found', 'server.conf')
logging.error('Error occurred')
logging.critical('Critical error -- shutting down')

Результат выполнения примера:

WARNING:root:Warning:config file server.conf not found
ERROR:root:Error occurred
CRITICAL:root:Critical error -- shutting down

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

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

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

Python имеет автоматическое управление памятью: подсчёт ссылок для большинства объектов и сборка мусора для устранения циклических ссылок. Память освобождается сразу после того, как была удалена последняя ссылка на объект.

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

>>> import weakref, gc
>>> class A:
...     def __init__(self, value):
...         self.value = value
...     def __repr__(self):
...         return str(self.value)
...
>>> a = A(10)                   # создать ссылку
>>> d = weakref.WeakValueDictionary()
>>> d['primary'] = a            # не создает ссылку
>>> d['primary']                # получить объект, если он еще жив
10
>>> del a                       # удалить одну ссылку
>>> gc.collect()                # немедленно запустить сборку мусора
0
>>> d['primary']                # запись была автоматически удалена
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    d['primary']                # запись была автоматически удалена
  File "C:/python38/lib/weakref.py", line 46, in __getitem__
    o = self.data[key]()
KeyError: 'primary'

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

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

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

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

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

>>> from collections import deque
>>> d = deque(["task1", "task2", "task3"])
>>> d.append("task4")
>>> print("Handling", d.popleft())
Handling task1
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 имеет функции для реализации кучи, основанной на обычных списках. Элемент с наименьшим значением всегда находится в позиции с индексом 0. Это свойство может с успехом быть задействовано в приложениях, где особенно частый доступ необходим к наименьшему элементу, но полную сортировку проводить накладно:

>>> 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 сохраняет конечный ноль, автоматически выводя 4 значащие цифры из множителей с 2 значащими цифрами. 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
>>> sum([0.1]*10) == 1.0
False

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

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