# 11. Python 標準函式庫概覽——第二部份

第二部分涵蓋更多支援專業程式設計所需要的進階模組。這些模組很少出現在小腳本中。

## 11.1. 輸出格式化 (Output Formatting)

`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` 模組能存取一個含有特定文化相關資料格式的資料庫。locale 模組的 format 函式有一個 grouping 屬性，可直接以群分隔符 (group separator) 將數字格式化：

    >>> 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. 模板化 (Templating)

`string` 模組包含一個多功能的 `Template` class，提供的簡化語法適合使用者進行編輯時使用。它容許使用者客製化自己的應用程式，但不必對原應用程式做出變更。

格式化方式是使用佔位符號名稱 (placeholder name)，它是由 `$` 加上合法的 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.'

如果在 dictionary 或關鍵字引數中未提供某個佔位符號的值，那麼 `substitute()` method 將引發 `KeyError`。對於郵件合併 (mail-merge) 類型的應用程式，使用者提供的資料有可能是不完整的，此時使用 `safe_substitute()` method 會更適當——如果資料有缺少，它會保持佔位符號不變：

    >>> 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 的 subclass（子類別）可以指定自訂的分隔符號 (delimiter)。例如，一個相片瀏覽器的批次重新命名功能，可以選擇用百分號作為現在日期、照片序號或檔案格式的佔位符號：

    >>> 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. 二進制資料記錄編排 (Binary Data Record Layouts)

`struct` 模組提供了 `pack()` 和 `unpack()` 函式，用於處理可變動長度的二進制記錄格式。以下範例說明，如何在不使用 `zipfile` 模組的情況下，使用迴圈瀏覽一個 ZIP 檔案中的標頭資訊 (header information)。壓縮程式碼 `"H"` 和 `"I"` 分別代表兩個和四個位元組的無符號數 (unsigned number)。`"<"` 表示它們是標準大小，並使用小端 (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. 多執行緒 (Multi-threading)

執行緒是一種用來對非順序相依 (sequentially dependent) 的多個任務進行解耦 (decoupling) 的技術。當其他任務在背景執行時，多執行緒可以用來提升應用程式在接收使用者輸入時的反應能力。一個相關的用例是，運行 I/O 的同時，在另一個執行緒中進行計算。

以下程式碼顯示了高階的 `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 模組提供了多個同步處理原始物件 (synchronization primitive)，包括鎖 (lock)、事件 (event)、條件變數 (condition variable) 和號誌 (semaphore)。

儘管這些工具很強大，但很小的設計錯誤也可能導致一些難以重現的問題。所以，任務協調的首選方法是，把所有對資源的存取集中到單一的執行緒中，然後使用 `queue` 模組向該執行緒饋送來自其他執行緒的請求。應用程式若使用 `Queue` 物件進行執行緒間的通信和協調，會更易於設計、更易讀、更可靠。

## 11.5. 日誌記錄 (Logging)

`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

在預設情況，資訊和除錯訊息不會被顯示，其輸出會被發送到標準錯誤 (standard error)。其他輸出選項包括，將訊息轉發到電子郵件、資料報 (datagram)、網路插座 (socket) 或 HTTP 伺服器。新的過濾器可以根據訊息的優先順序，選擇不同的路由 (routing) 方式：`DEBUG`，`INFO`，`WARNING`，`ERROR`，及 `CRITICAL`。

日誌記錄系統可以直接從 Python 配置，也可以透過載入使用者可編輯的配置檔案以進行客製化的日誌記錄，而無須對應用程式做出變更。

## 11.6. 弱引用 (Weak References)

Python 會自動進行記憶體管理（對大多數物件進行參照計數 (reference counting) 並使用 garbage collection 來消除循環參照）。當一個參照從記憶體被移除後不久，該記憶體就會被釋出。

此方式對大多數應用程式來說都沒問題，但偶爾也需要在物件仍然被其他物件所使用時持續追蹤它們。可惜的是，儘管只是追蹤它們，也會建立一個使它們永久化 (permanent) 的參照。`weakref` 模組提供的工具可以不必建立參照就能追蹤物件。當該物件不再被需要時，它會自動從一個弱引用表 (weakref table) 中被移除，並為弱引用物件觸發一個回呼 (callback)。典型的應用包括暫存 (cache) 那些成本較為昂貴的物件：

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

## 11.7. 使用於 List 的工具

許多對於資料結構的需求，可以透過內建的 list（串列）型別來滿足。但是，有時也會根據效能的各種取捨，需要一些替代的實作。

`array` 模組提供了一個 `array()` 物件，它像是 list，但只能儲存同類的資料且能緊密地儲存。下面的範例展示一個數值陣列 (array)，以兩個位元組的無符號二進數 (unsigned binary numbers) 為儲存單位（類型碼為 `"H"`），而在 Python 整數物件的正規 list 中，每個項目通常使用 16 個位元組：

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

`collections` 模組提供了一個 `deque()` 物件，它像是 list，但從左側加入 (append) 和彈出 (pop) 的速度較快，而在中間查找的速度則較慢。這種物件適用於實作佇列 (queue) 和廣度優先搜尋法 (breadth first tree 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)

除了替代的 list 實作以外，函式庫也提供了其他工具，例如 `bisect` 模組，具有能夠操作 sorted list（已排序串列）的函式：

    >>> 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` 模組提供了一些函式，能基於正規 list 來實作堆積 (heap)。最小值的項目會永遠保持在位置零。對於一些需要多次存取最小元素，但不想要對整個 list 進行排序的應用程式來說，這會很有用：

    >>> 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` 模組提供了一個 `Decimal` 資料類型，用於十進制浮點數運算。相較於內建的二進制浮點數 `float` 實作，該 class 特別適用於下列情境

- 金融應用程式及其他需要準確十進位制表示法的應用，

- 對於精確度 (precision) 的控制，

- 控制四捨五入，以滿足法律或監管規範，

- 追蹤有效的小數位數 (decimal place)，或

- 使用者會期望計算結果與手工計算相符的應用程式。

例如，要計算 70 美分的手機充電加上 5％ 稅金的總價，使用十進制浮點數和二進制浮點數，會算出不同的答案。如果把計算結果四捨五入到最接近的美分，兩者的差異會更顯著：

    >>> from decimal import *
    >>> round(Decimal('0.70') * Decimal('1.05'), 2)
    Decimal('0.74')
    >>> round(.70 * 1.05, 2)
    0.73

`Decimal` 的運算結果會保留尾部的零，也會根據有兩個有效位數的被乘數自動推斷出有四個有效位數的乘積。Decimal 可以重現手工計算的結果，以避免產生二進制浮點數無法準確表示十進制數值時會導致的問題。

準確的表示法使得 `Decimal` class 能夠執行對於二進制浮點數不適用的模數計算和相等性檢測：

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