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 забезпечує прямий спосіб форматування чисел за допомогою роздільників груп:

>>> 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, коли покажчик місця заповнення не вказано в словнику або в аргументі ключового слова. Для програм у стилі злиття дані, надані користувачем, можуть бути неповними, і метод 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:/python312/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
>>> 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')