Python 3 Tutorial 第六堂(2)使用 with as


在〈Python 3 Tutorial 第五堂(1)Shit happens!〉中談過,可以使用 finally 來做一些資源收尾動作,其中的範例是:

import sys, logging

def for_each_line(file, do_action):
    try:
        for line in file:
            do_action(line)
    except:
        logger = logging.getLogger(__name__)
        logger.exception('未處理的例外')
    finally:
        file.close()

try:
    file = open(sys.argv[1], 'r')
    for_each_line(file, lambda line: print(int(line) + 10, end = ''))
except IndexError:
    print('請提供檔案名稱')
    print('範例:')
    print('    python3.5 read.py your_file')
except FileNotFoundError:
    print('找不到檔案 {0}'.format(sys.argv[1]))

自定義 with_as

實際上,使用 finally 來關閉資源的流程大同小異,你也許會想要定義一個更通用的 with_as 函式,來重用這個流程:

import sys, logging

def with_as(file, do_action):
    try:
        do_action(file)
    finally:
        file.close()

def print_each_line(file):
    try:
        for line in file:
            print(line, end = '')
    except:
        logger = logging.getLogger(__name__)
        logger.exception('未處理的例外')

try:
    with_as(open(sys.argv[1], 'r'), print_each_line)
except IndexError:
    print('請提供檔案名稱')
    print('範例:')
    print('    python3.5 read.py your_file')
except FileNotFoundError:
    print('找不到檔案 {0}'.format(sys.argv[1]))

如上頭所示,有了這個自定義的 with_as,日後你就可以像 with_as(open(sys.argv[1], 'r'), print_each_line) 這樣,直接重用資源關閉的流程。

with as 語法

實際上,Python 中就提供了 with as 語法,來看看採用這個語法後,上面的程式看起來會長什麼樣子:

import sys, logging

def print_each_line(file):
    try:
        for line in file:
            print(line, end = '')
    except:
        logger = logging.getLogger(__name__)
        logger.exception('未處理的例外')

try:
    with open(sys.argv[1], 'r') as file:
        print_each_line(file)
except IndexError:
    print('請提供檔案名稱')
    print('範例:')
    print('    python3.5 read.py your_file')
except FileNotFoundError:
    print('找不到檔案 {0}'.format(sys.argv[1]))

如果你不需要 print_each_line 函式,也可以直接寫成這樣:

import sys, logging

try:
    with open(sys.argv[1], 'r') as file:
        for line in file:
            print(line, end = '')
except IndexError:
    print('請提供檔案名稱')
    print('範例:')
    print('    python3.5 read.py your_file')
except FileNotFoundError:
    print('找不到檔案 {0}'.format(sys.argv[1]))
except:
    print('未知的錯誤,請洽管理員')
    logger = logging.getLogger(__name__)
    logger.exception('未處理的例外')

with as 負責關閉資源,若發生了例外,要看資源本身在定義時,是否抑制了例外,open 開啟的檔案來說是沒有抑制例外,因此在上面的例子中,若中間發生了例外,with as 會關閉資源,然後例外依舊向外傳播,而程式中捕捉例外後,以 print 顯示錯誤訊息給使用者觀看。

with as 原理

實際上,with as 不限使用於檔案,只要物件支援環境管理協定(Context Management Protocol),就可以使用 with as 語句。支援環境管理協定的物件,必須實作 __enter__()__exit__() 兩個方法,這樣的物件稱之為環境管理員(Context Manager)。

with 陳述句一開始執行,就會進行 __enter__() 方法,該方法傳回的物件,可以使用 as 指定給變數(如果有的話),接著就執行 with 區塊中的程式碼,以下是個簡單示範:

class Resource:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print(self.name, ' __enter__')
        return self

    def __exit__(self, type, value, traceback):
        print(self.name, ' __exit__')
        return False

with Resource('res') as resource:
    print(resource.name)

如果 with 區塊中的程式碼發生了例外,則會執行 __exit__() 方法,並傳入三個引數,這三個引數的意義是例外類型、例外訊息以及 traceback 物件(可參考〈再看 try、raise〉中的說明)。此時 __exit__() 方法若傳回 False,則例外會被重新丟出,否則例外就停止傳播,通常 __exit__() 會傳回 False,以便在 with 之外還可以處理例外。

如果 with 區塊中沒有發生例外而執行完畢,則也是執行 __exit__() 方法,此時 __exit__() 的三個參數都接收到None。就上面的例子來說,會如下依序顯示:

res  __enter__
res
res  __exit__

練習 9:使用 with as

在練習 8 的 pickle_ex.py 與 shelve_ex.py 原始碼中,是自行使用 finally 來關閉資源,請試著將之改寫為 with as 來自動關閉資源。