11. مروری کوتاه بر کتابخانهٔ استاندارد — بخش دوم

این بخش دوم از مرور، ماژول‌های پیشرفته‌تری را معرفی می‌کند که نیازهای برنامه‌نویسی حرفه‌ای را پوشش می‌دهند. این ماژول‌ها به‌ندرت در اسکریپت‌های کوچک استفاده می‌شوند.

11.1. قالب‌بندی خروجی

ماژول reprlib نسخه‌ای از تابع repr() را ارائه می‌دهد که برای نمایش خلاصه‌شدهٔ ظرف‌های بزرگ یا دارای تو‌در‌تویی عمیق سفارشی شده است:

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

ماژول pprint کنترل پیشرفته‌تری برای چاپ اشیای داخلی و اشیای تعریف‌شده توسط کاربر، به شکلی که برای مفسر قابل‌خواندن باشد، فراهم می‌کند. اگر خروجی بیش از یک خط باشد، «چاپگر زیبا» (pretty printer) با افزودن شکست خط و تورفتگی، ساختار داده را واضح‌تر نمایش می‌دهد:

>>> 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_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 با نحوی ساده است که برای ویرایش توسط کاربران نهایی مناسب است. این امکان را فراهم می‌کند که کاربران بتوانند برنامه‌های خود را بدون تغییر دادن خود برنامه سفارشی‌سازی کنند.

در این قالب، نام جای‌نگهدارها با استفاده از $ و شناسه‌های معتبر پایتون (شامل حروف، ارقام و زیرخط) ساخته می‌شوند. اگر جای‌نگهدار را درون آکولاد قرار دهید، می‌توانید بدون فاصله، بلافاصله پس از آن حروف یا ارقام بیشتری بیاورید. همچنین، نوشتن $$ یک نویسهٔ $ فراردهی‌شده ایجاد می‌کند:

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

زیرکلاس‌های 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() را برای کار با قالب‌های دودویی رکوردهایی با طول متغیر فراهم می‌کند. مثال زیر نشان می‌دهد چگونه می‌توان بدون استفاده از ماژول zipfile، اطلاعات سرآیند یک فایل ZIP را پیمایش کرد. کدهای "H" و "I" به‌ترتیب نشان‌دهندهٔ اعداد بدون علامتِ دو و چهار بایتی هستند. نماد "<" نیز نشان می‌دهد که اندازه‌ها استاندارد بوده و ترتیب بایت‌ها little-endian است:

import struct

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

start = 0
for i in range(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):
        super().__init__()
        self.infile = infile
        self.outfile = outfile

    def run(self):
        with zipfile.ZipFile(self.outfile, 'w', zipfile.ZIP_DEFLATED) as f:
            f.write(self.infile)
        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()    # منتظر می‌ماند تا وظیفهٔ پس‌زمینه پایان یابد
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.

سامانهٔ ثبت رویداد را می‌توان مستقیماً از داخل پایتون پیکربندی کرد یا تنظیمات آن را از یک فایل پیکربندی قابل ویرایش توسط کاربر بارگذاری نمود تا بدون تغییر برنامه، رفتار ثبت رویدادها سفارشی شود.

11.6. ارجاع‌های ضعیف

پایتون مدیریت حافظه را به‌صورت خودکار انجام می‌دهد (برای بیشتر اشیا با استفاده از شمارش ارجاع‌ها و garbage collection (جمع‌آوری زباله) برای حذف چرخه‌ها). حافظه اندکی پس از حذف آخرین ارجاع به یک شی آزاد می‌شود.

این روش برای بیشتر برنامه‌ها مناسب است، اما گاهی لازم است اشیا فقط تا زمانی دنبال شوند که توسط چیز دیگری در حال استفاده باشند. مشکل اینجاست که صرفِ دنبال کردن یک شی، خود یک ارجاع ایجاد می‌کند و باعث می‌شود آن شی هرگز آزاد نشود. ماژول weakref ابزارهایی برای دنبال کردن اشیا بدون ایجاد ارجاع فراهم می‌کند. هنگامی که دیگر نیازی به یک شی نباشد، آن شی به‌طور خودکار از جدول weakref حذف می‌شود و یک callback برای اشیای weakref فراخوانی می‌شود. یکی از کاربردهای رایج این قابلیت، ذخیرهٔ موقت (cache) اشیایی است که ایجاد آن‌ها پرهزینه است:

>>> 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:/python314/lib/weakref.py", line 46, in __getitem__
    o = self.data[key]()
KeyError: 'primary'

11.7. ابزارهایی برای کار با فهرست‌ها

بسیاری از نیازهای مربوط به ساختارهای داده را می‌توان با استفاده از نوع داخلی list برآورده کرد. با این حال، گاهی به پیاده‌سازی‌های جایگزینی نیاز است که توازن متفاوتی میان کارایی و عملکرد ارائه می‌دهند.

ماژول array شیء array را فراهم می‌کند که شبیه یک فهرست است، اما تنها داده‌های هم‌نوع را ذخیره می‌کند و آن‌ها را به‌شکلی فشرده‌تر در حافظه نگه می‌دارد. مثال زیر آرایه‌ای از اعداد را نشان می‌دهد که به‌جای ذخیره شدن به‌صورت اعداد دودویی بدون علامتِ دو بایتی (با کد نوع "H")، به‌صورت اشیای معمولی int پایتون (که معمولاً برای هر عضو ۱۶ بایت فضا اشغال می‌کنند) ذخیره شده‌اند:

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

ماژول collections شیء deque را فراهم می‌کند که شبیه یک فهرست است، اما افزودن و حذف عنصر از ابتدای آن بسیار سریع‌تر و دسترسی به عناصر میانی آن کندتر است. این اشیا برای پیاده‌سازی صف‌ها و پیمایش درخت به روش جست‌وجوی سطح‌به‌سطح (Breadth-First Search) بسیار مناسب هستند:

>>> 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 توابعی برای پیاده‌سازی هیپ (Heap) بر پایهٔ فهرست‌های معمولی فراهم می‌کند. در این ساختار، کوچک‌ترین عنصر همیشه در اندیس صفر قرار می‌گیرد. این ویژگی برای برنامه‌هایی مفید است که به‌طور مکرر به کوچک‌ترین عنصر نیاز دارند، اما نمی‌خواهند هر بار کل فهرست را مرتب کنند:

>>> 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 که از ممیز شناور دودویی استفاده می‌کند، این کلاس به‌ویژه در موارد زیر مفید است

  • برنامه‌های مالی و سایر کاربردهایی که به نمایش دقیق اعداد ده‌دهی نیاز دارند،

  • کنترل میزان دقت،

  • کنترل نحوهٔ گرد کردن اعداد برای انطباق با الزامات یا مقررات،

  • حفظ و ردیابی تعداد ارقام معنادار اعشاری، یا

  • برنامه‌هایی که کاربر انتظار دارد نتایج آن‌ها با محاسبات دستی مطابقت داشته باشد.

برای مثال، محاسبهٔ مالیات ۵٪ برای هزینهٔ ۷۰ سنتی یک تماس تلفنی، در محاسبات اعشاری و محاسبات دودویی با ممیز شناور نتایج متفاوتی ایجاد می‌کند. این تفاوت زمانی که نتایج به نزدیک‌ترین سنت گرد شوند، اهمیت پیدا می‌کند:

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