首頁 > 軟體

Python上下文管理器詳細使用教學

2023-02-08 22:01:22

with語句會設定一個臨時的上下文,交給上下文管理器物件控制,並且負責清理上下問題。

這樣做能避免錯誤並減少樣板程式碼,因此API能更安全,更易使用。除了自動關閉檔案之外,with塊還有很多用途。

上下文管理器和with塊

上下文管理器物件目的是管理with語句,就像迭代器的存在是為了管理for語句一樣。

with語句的目的是簡化try/finnally模式。

這種模式用於保證一段程式碼執行完畢後執行某項操作,即便那段程式碼是由於異常、return語句、sys.exit()呼叫而終止的,也都會執行指定的finally操作。finally子句中通常存放用於釋放重要資源,或者還原臨時變更的狀態。

上下文管理器協定包含__enter__和__exit__兩個方法。

with語句開始執行時,會在上下文管理物件上呼叫__enter__方法;with語句執行結束之後,會在上下文管理物件上呼叫__exit__方法,以扮演finally子句的角色。

範例,把檔案物件當做上下文管理器物件使用。

with open('cafe.txt') as fp:
    src = fp.read(60)
print(fp)  # fp變數依舊可以用
print(fp.closed, fp.encoding)  # 讀取fp物件的屬性
print(fp.read())  # 但是執行fp的IO操作會異常

列印
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'>
True cp936
Traceback (most recent call last):
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 8, in <module>
    print(fp.read())
ValueError: I/O operation on closed file.

知識點:

  • fp變數在上下文管理器之外,依舊存在,可以讀取fp物件屬性。因為with塊於函數和模組不同,沒有定義新的作用域。
  • 但是 不能在fp上再執行IO操作,因為在with塊的末尾,已經呼叫了TextIOWrapper__exit__方法把檔案關閉了。

執行with後面的表示式的結果是上下文管理器物件,不過,把值繫結到目標變數(as後的變數)是在上下文管理器物件上呼叫__enter__方法的結果。

不管控制流程以哪種方式退出with塊,都會在上下文管理器物件上呼叫__exit__方法,而不是在__enter__方法返回的物件上呼叫。

with語句的as子句是可選的。對於像open這樣的函數來說,必須加上as子句,以便獲取檔案的物件參照。不過一些上下文管理器物件會返回None,因為沒有什麼有用的物件給使用者提供。

範例,實現一個LookingGlass類,上下文管理器

class LookingGlass:
    """鏡子:看到的字是反的"""
    def __enter__(self):
        import sys
        self.original_write = sys.stdout.write  # 把原始的[螢幕列印輸出]函數儲存到一個範例屬性中,供以後使用
        sys.stdout.write = self.reverse_write  # 猴子修補程式:替換成自己的方法實現
        return "ABCD"
    def reverse_write(self, text):
        self.original_write(text[::-1])  # 呼叫原始的螢幕列印,但是把內容反轉
    def __exit__(self, exc_type, exc_val, exc_tb):
        import sys
        sys.stdout.write = self.original_write  # 還原成原始的函數功能
        if exc_type is ZeroDivisionError:
            print('Do not divide by Zero!')
            return True  # 告訴直譯器,異常已經處理
        # 其他的情況返回None,交給Python丟擲異常
with LookingGlass() as what:
    print('lijiachang')
    print(what)
print(what)
print('back to normal')

列印
gnahcaijil
DCBA
ABCD
back to normal

知識點:

  • sys.stdout.write 是標準螢幕列印輸出。要注意如果暫時快取其他物件改變功能,記得最後還原成原來的版本。
  • Python呼叫__enter__方法時,除了self之外不會傳入其他引數
  • 如果一切正常,Python呼叫__exit__方法時,傳入的是None,None,None;如果丟擲了異常,這三個引數是異常資料。如下:

exec_type: 異常類名稱。如ZeroDivisionError

exc_value: 異常範例。有時會有引數傳遞給異常構造方法,例如錯誤資訊,這些引數使用exc_value.args獲取

traceback: traceback物件。

補充,在try/finally語句的finally塊中呼叫sys.exc_info()得到的就是__exit__接收的這三個引數。

  • 在__exit__中返回True是告訴直譯器,異常已經處理了。如果返回True之外的值,比如預設的None,with塊中的異常會向上冒泡,讓Python來丟擲。

In [44]: manager = LookingGlass()

In [45]: manager

Out[45]: <__main__.LookingGlass at 0xac6a970>

In [46]: m = manager.__enter__()

In [47]: m == "ABCD"

Out[47]: eurT

In [49]: m

Out[49]: 'DCBA'

In [50]: manager

Out[50]: >079a6cax0 ta ssalGgnikooL.__niam__<

In [54]: manager.__exit__(None, None, None)

In [55]: m

Out[55]: 'ABCD'

可以看到在呼叫__enter__之後,所有的標準列印輸出,都會反轉。因為stdout的所有輸出都經過了__enter__方法中打修補程式的reverse_write方法實現。

contextlib模組

Python標準庫檔案中的contextlib模組,提供了自定義上下文管理器的一些函數

  • closing :如果物件提供了close()方法,但沒有實現__enter__/__exit__方法,可以使用這個函數來構建上下文管理器。
  • suppress : 構建臨時忽略指定異常的上下文。
  • @contextmanager :這個裝飾器可以把簡單的生成器變為上下文管理器,這樣就不需要建立類來實現管理器協定了。
  • ContextDecorate :這是個基礎類別,用於編寫類可以繼承他,用於定義基於類的上下文管理器。也可以用於裝飾器函數,在受管理的上下文中執行整個函數。
  • ExitStack : 這個上下文管理器能進入多個上下文管理器。with塊結束時,按照後進先出的順序呼叫棧中各個上下文管理器的__exit__方法。如果事先不知道 with塊要進入多少個上下文管理器,可以使用這個類。

使用最廣泛的還是@contextmanager裝飾器。要注意,這個裝飾器與迭代無關,卻要使用yeild關鍵字。

@contextmanager 裝飾器

使用@contextmanager裝飾器呢個減少建立上下文管理器的程式碼量,因為不用編寫一個完整的類,不用定義__enter__和__exit__方法,只需要一個實現yeild語句的生成器,生成想讓__enter__方法返回的值。

其中yeild語句的作用是把函數的定義體分為兩部分:

yeild語句前面的程式碼在with塊開始時(即直譯器呼叫__enter__方法時)執行。

yeild語句後面的程式碼在with塊結束時(即呼叫__exit__方法時)執行。

範例,使用生成器實現上下文管理器

import contextlib
@contextlib.contextmanager
def looking_glass():
    import sys
    original_write = sys.stdout.write  # 把原始的[螢幕列印輸出]函數儲存到一個範例屬性中,供以後使用
    def reverse_write(text):
        original_write(text[::-1])
    sys.stdout.write = reverse_write  # 猴子修補程式:替換成自己的方法實現
    yield "ABCD"  # 這個值會繫結到as後的變數上
    sys.stdout.write = original_write
with looking_glass() as what:
    print('lijiachang')
    print(what)
print(what)
print('back to normal')

知識點 :

  • yield 後面的值會繫結到with語句中as子句的目標變數上,執行with塊中的程式碼,這個函數會在這裡暫停。
  • 控制權一旦調成with塊,就會繼續執行yeild語句後的程式碼

@contextmanager 原理和注意事項

其實,contextlib.contextmanager裝飾器會把函數包裝實現成__enter__和__exit__方法的類。(ps:類的名字叫_GeneratorContextManager)

這個類的__enter__方法有如下作用:

  • 呼叫生成器函數,儲存生成器物件(這裡把他稱為gen)。
  • 呼叫next(gen),執行到yeild關鍵字所在的位置。
  • 返回next(gen)產出的值,把產出的值繫結到with/as語句的目標變數上。

with塊終止時,__exit__方法會做以下事情:

  • 檢查有沒有異常傳給exc_type:
  • 如果有,就呼叫gen.throw(exception), 在生成器函數定義體中包含yeild關鍵字的那一行丟擲異常。
  • 如果沒有異常,再次呼叫next(gen),繼續執行定義體中yeild語句之後的程式碼。

在上面的範例中,有一個嚴重的問題:如果在with塊中丟擲了異常,Python直譯器會捕獲,然後在looking_glass函數的yeild表示式再次丟擲。但是問題是沒有處理錯誤的程式碼,那麼looking_glass函數就會終止,永遠的無法恢復成sys.stdout.write方法原始個功能,導致系統的輸出處於無效狀態。

所以要新增一下異常的處理,比如ZeroDivisionError異常。

範例,新增例外處理的基於生成器的上下文管理器

import contextlib
@contextlib.contextmanager
def looking_glass():
    """鏡子:看到的字是反的"""
    import sys
    original_write = sys.stdout.write  # 把原始的[螢幕列印輸出]函數儲存到一個範例屬性中,供以後使用
    def reverse_write(text):
        original_write(text[::-1])
    sys.stdout.write = reverse_write  # 猴子修補程式:替換成自己的方法實現
    msg = ''
    try:
        yield "ABCD"  # 只需要捕獲yield部分
    except ZeroDivisionError:
        msg = 'do not divide by zero'
    finally:
        sys.stdout.write = original_write
        if msg:
            print(msg)
with looking_glass() as what:
    0 / 0  # 丟擲異常
    print('lijiachang')
    print(what)
print(what)
print('back to normal')

列印
do not divide by zero
ABCD
back to normal

知識點:

  • 在生成器函數中只需要捕獲yeild關鍵字這行的異常,Python直譯器會在with塊中的異常,轉移到yield這行丟擲。
  • 所以在使用@contextmanager裝飾器時,要把yeild語句放到try/finally語句中,因為我們永遠不知道上下文管理器的使用者會在with塊中做什麼。

關於異常的處理的對比:

  • 在用類實現上下文管理器時,前面說過,為了告訴直譯器異常已經處理過了,需要在__exit__方法中返回True,此時直譯器會壓制異常。如果__exit__沒有顯示的返回一個值,那麼直譯器得到的就是None,此時會向上冒泡異常。
  • 在用@contextmanager裝飾器時,預設的行為是相反的:裝飾器提供的__exit__方法假定發給生成器的所有異常都已經處理了,因此預設壓制異常。如果不想讓contextmanager壓制異常,必須在裝飾的函數中顯式的重新丟擲異常。

最後,再次強調:在@contextmanager裝飾器裝飾去生成器中,yield與迭代沒有任何關係。

到此這篇關於Python上下文管理器詳細使用教學的文章就介紹到這了,更多相關Python上下文管理器內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com