Python 模組介紹 - contextlib

Posted on  Nov 7, 2020  in  Python 程式設計 - 中階  by  Amo Chen  ‐ 3 min read

Python 淺談 with 語句 一文中,我們已介紹基本的 with 語法與何謂 context manager 以及簡單的 context manager 實作。

除了該文所提供的實作方法之外,也可以利用 Python 內建的 contextlib 模組進行開發,此外該模組也提供若干實用的 context manager 可以使用。

本文環境

  • Python 3.6.5

contextlib

contextlib 提供以 decorator 的方式讓開發者能夠輕鬆製作 context manager:

from contextlib import contextmanager


@contextmanager
def simple_context():
    print('enter')
    yield
    print('exit')


with simple_context():
    print('I am in the context')

上述範例執行結果如下:

enter
I am in the context
exit

context manager 很適合應用在需要對某段程式做處置的情況,例如衡量某段程式的執行時間,就能夠利用類似以下的 context manager 進行衡量:

from contextlib import contextmanager
from time import perf_counter
from random import randint


@contextmanager
def profiling():
    s = perf_counter()
    yield
    print('time: ', perf_counter() - s)


with profiling():
    for i in range(1000):
        randint(0, 10)

上述範例執行結果如下:

time:  0.0013769530000331542

不過上述等利用 yield 而做成的 context manager 並無法被重複使用,例如:

from contextlib import contextmanager
from time import perf_counter
from random import randint


@contextmanager
def profiling():
    s = perf_counter()
    yield
    print('time: ', perf_counter() - s)


profiler = profiling()
with profiler:
    for i in range(1000):
        randint(0, 10)


with profiler:
    for i in range(1000):
        randint(0, 10)

上述範例執行第 2 次(也就是第 19 行)就會出現以下錯誤:

time:  0.0012313259940128773
Traceback (most recent call last):
  File "....py", line 19, in <module>
    with profiler:
  File "/.../versions/3.6.5/lib/python3.6/contextlib.py", line 83, in __enter__
    raise RuntimeError("generator didn't yield") from None
RuntimeError: generator didn't yield

原因在於該 context manager 其實是 1 個 generator, 一旦該 generator 一結束就無法再被使用,也因此該種 context managet 被稱為 single use context manager .

如果要讓 context manager 能夠重複使用,就得以類別實作 __enter____exit__ 方法,例如以 class 形式修正前述範例後,該 profiler 就能夠正常執行多次:

class profiling(object):
    def __enter__(self):
        self.s = perf_counter()

    def __exit__(self, *exc):
        print('time: ', perf_counter() - self.s)


profiler = profiling()
with profiler:
    for i in range(1000):
        randint(0, 10)


with profiler:
    for i in range(1000):
        randint(0, 10)

上述執行結果:

time:  0.0013683729994227178
time:  0.0011790469980041962

其他內建的 context manager

closing

如果想讓某些有實作 close() 方法的類別,自動在離開 context manager 執行 close() 方法的話,可以使用contextlib.closing ,例如以下的 StringIO 在離開 closing context manager 時,自動執行了 close() 方法,因此導致第 8 行的 output.write('def') 無法正常寫入,因而發生錯誤:

import io
from contextlib import closing


with closing(io.StringIO()) as output:
    output.write('abc')

output.write('def')
Traceback (most recent call last):
  File "...", line 8, in <module>
    output.write('def')
ValueError: I/O operation on closed file

總的來說, contextlib.closing 相當適合需要確保 close() 方法一定得被呼叫的情境。

suppress

有些時候我們可能希望在程式內忽略某些無關緊要的 exception, 例如以下是忽略 ZeroDivisionError 的寫法:

try:
    x = 1 / 0
except ZeroDivisionError:
    pass

同樣的寫法可以換成使用 contextlib.suppress ,程式整體看起來也相較簡潔:

from contextlib import suppress


with suppress(ZeroDivisionError):
    x = 1 / 0

上述範例會忽略 ZeroDivisionError 例外錯誤。

suppress 也可以忽略多種例外錯誤,例如:

from contextlib import suppress


with suppress(ZeroDivisionError, ValueError):
    x = 1 / 0

ExitStack

contextlib 中有個有趣的 context manager 稱為 ExitStack .

該 context manager 的功用為讓開發者以堆疊(stack)的方式任意增加離開 context manager 時需要執行的函式(或者稱為 callback)。

也由於是 stack, 所以越晚增加的函示會先執行,例如以下範例:

from contextlib import ExitStack


def callback(*args, **kwargs):
    print('Callback', args[0])


def do_something():
    return True


with ExitStack() as stack:
    stack.callback(callback, 'a')
    stack.callback(callback, 'b')
    result = do_something()

上述範例執行結果如下,可以看到 stack.callback(callback, 'b') 最後被加入,也最先被執行:

Callback b
Callback a

另外, ExitStack 也可以選擇不執行 stack 內所有的 callback, 只要在離開 context manager 之前呼叫 pop_all() 即可,這也提供某種程度上的彈性,讓我們能夠控制程式流程:

from contextlib import ExitStack


def callback(*args, **kwargs):
    print('Callback', args[0])


def do_something():
    return True


with ExitStack() as stack:
    stack.callback(callback, 'a')
    stack.callback(callback, 'b')
    result = do_something()
    if result:
        stack.pop_all()

上述範例由於離開 context manager 之前呼叫 stack.pop_all() , 因此任何 callback 都不會執行。

以上就是關於 contextlib 的內容,官方文件中也紀錄其他本文未提及的 context manager, 可以花些時間翻閱,也許有些能夠運用在日常開發之中。

Happy coding!

References

https://docs.python.org/3/library/contextlib.html

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!