11. Краткий экскурс в стандартную библиотеку — Часть II

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

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

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

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

Модуль 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 функции locale’s format обеспечивает прямой способ форматирования чисел с групповыми разделителями:

>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'English_United States.1252')
'English_United States.1252'
>>> conv = locale.localeconv()          # get a mapping of conventions
>>> x = 1234567.8
>>> locale.format_string("%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, если в словаре или в аргументе ключевого слова не указано местодержатель. Для приложений в стиле mail-merge данные, предоставляемые пользователем, могут быть неполными, и метод 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.'

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

>>> 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" представляют собой двух- и четырехбайтовые беззнаковые числа соответственно. Код "<" указывает на то, что они имеют стандартный размер и младший порядок байт:

import struct

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

start = 0
for i in range(3):                      # show the first 3 file headers
    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     # skip to the next header

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('Finished background zip of:', self.infile)

background = AsyncZip('mydata.txt', 'myarchive.zip')
background.start()
print('The main program continues to run in foreground.')

background.join()    # Wait for the background task to finish
print('Main program waited until background was done.')

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

Хотя эти инструменты очень мощные, незначительные ошибки в проектировании могут привести к проблемам, которые трудно воспроизвести. Поэтому предпочтительным подходом к координации задач является сосредоточение всего доступа к ресурсу в одном потоке, а затем использование модуля 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 осуществляет автоматическое управление памятью (подсчет ссылок для большинства объектов и garbage collection для устранения циклов). Память освобождается вскоре после того, как последняя ссылка на нее будет удалена.

Такой подход отлично подходит для большинства приложений, но иногда возникает необходимость отслеживать объекты только до тех пор, пока они используются чем-то другим. К сожалению, простое отслеживание создает ссылку, которая делает их постоянными. Модуль weakref предоставляет инструменты для отслеживания объектов без создания ссылки. Когда объект больше не нужен, он автоматически удаляется из таблицы weakref, а для объектов 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:/python314/lib/weakref.py", line 46, in __getitem__
    o = self.data[key]()
KeyError: 'primary'

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

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

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

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

>>> from heapq import heapify, heappop, heappush
>>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
>>> heapify(data)                      # rearrange the list into heap order
>>> heappush(data, -5)                 # add a new entry
>>> [heappop(data) for i in range(3)]  # fetch the three smallest entries
[-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('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')