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

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

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:/python310/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
>>> sum([0.1]*10) == 1.0
False

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

>>> getcontext().prec = 36
>>> Decimal(1) / Decimal(7)
Decimal('0.142857142857142857142857142857142857')
Вернуться на верх