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()          # 取得慣例的對映
   >>> 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. 模板化 (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):                      # 顯示前 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. 多執行緒 (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()    # 等待背景任務結束
   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)                   # 建立一個參照
   >>> 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:/python313/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)                      # 將串列重新排列為堆積順序
   >>> heappush(data, -5)                 # 加入新項目
   >>> [heappop(data) for i in range(3)]  # 取得三個最小的項目
   [-5, 0, 1]


11.8. 十進制浮點數運算 (Decimal Floating-Point Arithmetic)
==========================================================

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