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 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("%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.'

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

>>> 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.')

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

>>> 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')
Вернуться на верх