詳解利用上下文管理器擴展Python計時器

上文中,我們一起學習瞭手把手教你實現一個 Python 計時器。本文中,雲朵君將和大傢一起瞭解什麼是上下文管理器 和 Python 的 with 語句,以及如何完成自定義。然後擴展 Timer 以便它也可以用作上下文管理器。最後,使用 Timer 作為上下文管理器如何簡化我們自己的代碼。

上文中我們創建的第一個 Python 計時器類,然後逐步擴展我們 Timer 類,其代碼也是較為豐富強大。我們不能滿足於此,仍然需要模板一些代碼來使用Timer

  • 首先,實例化類
  • 其次,在要計時的代碼塊之前調用 .start()
  • 最後,在代碼塊之後調用 .stop()

一個 Python 定時器上下文管理器

Python 有一個獨特的構造,用於在代碼塊之前和之後調用函數:上下文管理器

瞭解 Python 中的上下文管理器

上下文管理器長期以來一直是 Python 中重要的一部分。由 PEP 343 於 2005 年引入,並首次在 Python 2.5 中實現。可以使用 with 關鍵字識別代碼中的上下文管理器:

with EXPRESSION as VARIABLE:
    BLOCK

EXPRESSION 是一些返回上下文管理器的 Python 表達式。首先上下文管理器綁定到變量名 VARIABLE上,BLOCK 可以是任何常規的 Python 代碼塊。上下文管理器保證程序在 BLOCK 之前調用一些代碼,在 BLOCK 執行之後調用一些其他代碼。這樣即使 BLOCK 引發異常,後者也是會照樣執行。

上下文管理器最常見的用途是處理不同的資源,如文件、鎖和數據庫連接等。上下文管理器用於使用資源後釋放和清理資源。以下示例僅通過打印包含冒號的行來演示 timer.py 的基本結構。此外,它展示瞭在 Python 中打開文件的常用習語:

with open("timer.py") as fp:
    print("".join(ln for ln in fp if ":" in ln))

class TimerError(Exception):
class Timer:
    timers: ClassVar[Dict[str, float]] = {}
    name: Optional[str] = None
    text: str = "Elapsed time: {:0.4f} seconds"
    logger: Optional[Callable[[str], None]] = print
    _start_time: Optional[float] = field(default=None, init=False, repr=False)
    def __post_init__(self) -> None:
        if self.name is not None:
    def start(self) -> None:
        if self._start_time is not None:
    def stop(self) -> float:
        if self._start_time is None:
        if self.logger:
        if self.name:

註意,使用 open() 作為上下文管理器,文件指針fp 不會顯式關閉,可以確認 fp 已自動關閉:

fp.closed

True

在此示例中,open("timer.py") 是一個返回上下文管理器的表達式。該上下文管理器綁定到名稱 fp。上下文管理器在 print() 執行期間有效。這個單行代碼塊在 fp 的上下文中執行。

fp 是上下文管理器是什麼意思? 從技術上講,就是 fp 實現瞭 上下文管理器協議。Python 語言底層有許多不同的協議。可以將協議視為說明我們代碼必須實現哪些特定方法的合同。

上下文管理器協議由兩種方法組成:

  • 進入與上下文管理器相關的上下文時調用 .__enter__()
  • 退出與上下文管理器相關的上下文時調用 .__exit__()

換句話說,要自己創建上下文管理器,需要編寫一個實現 .__enter__() 和 .__exit__() 的類。試試 Hello, World!上下文管理器示例:

# studio.py
class Studio:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print(f"你好 {self.name}")
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        print(f"一會兒見, {self.name}")

Studio是一個上下文管理器,它實現瞭上下文管理器協議,使用如下:

from studio import Studio
with Studio("雲朵君"):
    print("正在忙 ...")

你好 雲朵君
正在忙 …
一會兒見, 雲朵君

首先,註意 .__enter__() 在做事之前是如何被調用的,而 .__exit__() 是在做事之後被調用的。該示例中,沒有引用上下文管理器,因此不需要使用 as 為上下文管理器命名。

接下來,註意 self.__enter__() 的返回值受 as 約束。創建上下文管理器時,通常希望從 .__enter__() 返回 self 。可以按如下方式使用該返回值:

from greeter import Greeter
with Greeter("雲朵君") as grt:
  print(f"{grt.name} 正在忙 ...")

你好 雲朵君
雲朵君 正在忙 …
一會兒見, 雲朵君

在寫 __exit__ 函數時,需要註意的事,它必須要有這三個參數:

  • exc_type:異常類型
  • exc_val:異常值
  • exc_tb:異常的錯誤棧信息

這三個參數用於上下文管理器中的錯誤處理,它們以 sys.exc_info() 的返回值返回。當主邏輯代碼沒有報異常時,這三個參數將都為None。

如果在執行塊時發生異常,那麼代碼將使用異常類型、異常實例和回溯對象(即exc_typeexc_valueexc_tb)調用 .__exit__() 。通常情況下,這些在上下文管理器中會被忽略,而在引發異常之前調用 .__exit__()

from greeter import Greeter
with Greeter("雲朵君") as grt:
    print(f"{grt.age} does not exist")

你好 雲朵君
一會兒見, 雲朵君
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
AttributeError: 'Greeter' object has no attribute 'age'

可以看到,即使代碼中有錯誤,還是照樣打印瞭 "一會兒見, 雲朵君"

理解並使用 contextlib

現在我們初步瞭解瞭上下文管理器是什麼以及如何創建自己的上下文管理器。在上面的例子中,我們隻是為瞭構建一個上下文管理器,卻寫瞭一個類。如果隻是要實現一個簡單的功能,寫一個類未免有點過於繁雜。這時候,我們就想,如果隻寫一個函數就可以實現上下文管理器就好瞭。

這個點Python早就想到瞭。它給我們提供瞭一個裝飾器,你隻要按照它的代碼協議來實現函數內容,就可以將這個函數對象變成一個上下文管理器。

我們按照 contextlib 的協議來自己實現一個上下文管理器,為瞭更加直觀我們換個用例,創建一個我們常用且熟悉的打開文件(with open)的上下文管理器。

import contextlib

@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')
 
    # 【重點】:yield
    yield file_handler

    # __exit__方法
    print('close file:', file_name, 'in __exit__')
    file_handler.close()
    return

with open_func('test.txt') as file_in:
    for line in file_in:
        print(line)

在被裝飾函數裡,必須是一個生成器(帶有yield),而 yield 之前的代碼,就相當於__enter__裡的內容。yield 之後的代碼,就相當於__exit__ 裡的內容。

上面這段代碼隻能實現上下文管理器的第一個目的(管理資源),並不能實現第二個目的(處理異常)。

如果要處理異常,可以改成下面這個樣子。

import contextlib

@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')

    try:
        yield file_handler
    except Exception as exc:
        # deal with exception
        print('the exception was thrown')
    finally:
        print('close file:', file_name, 'in __exit__')
        file_handler.close()
        return

with open_func('test.txt') as file_in:
    for line in file_in:
        1/0
        print(line)

Python 標準庫中的 contextlib包括定義新上下文管理器的便捷方法,以及可用於關閉對象、抑制錯誤甚至什麼都不做的現成上下文管理器!

創建 Python 計時器上下文管理器

瞭解瞭上下文管理器的一般工作方式後,要想知道它們是如何幫助處理時序代碼呢?假設如果可以在代碼塊之前和之後運行某些函數,那麼就可以簡化 Python 計時器的工作方式。其實,上下文管理器可以自動為計時時顯式調用 .start() 和.stop()

同樣,要讓 Timer 作為上下文管理器工作,它需要遵守上下文管理器協議,換句話說,它必須實現 .__enter__()  .__exit__() 方法來啟動和停止 Python 計時器。從目前的代碼中可以看出,所有必要的功能其實都已經可用,因此隻需將以下方法添加到之前編寫的的 Timer 類中即可:

# timer.py
@dataclass
class Timer:
    # 其他代碼保持不變

    def __enter__(self):
        """Start a new timer as a context manager"""
        self.start()
        return self

    def __exit__(self, *exc_info):
        """Stop the context manager timer"""
        self.stop()

Timer 現在就是一個上下文管理器。實現的重要部分是在進入上下文時, .__enter__() 調用 .start() 啟動 Python 計時器,而在代碼離開上下文時, .__exit__() 使用 .stop() 停止 Python 計時器。

from timer import Timer
import time
with Timer():
    time.sleep(0.7)

Elapsed time: 0.7012 seconds

此處註意兩個更微妙的細節:

  • .__enter__() 返回 self,Timer 實例,它允許用戶使用 as 將 Timer 實例綁定到變量。例如,使用 with Timer() as t: 將創建指向 Timer 對象的變量 t
  • .__exit__() 需要三個參數,其中包含有關上下文執行期間發生的任何異常的信息。代碼中,這些參數被打包到一個名為 exc_info 的元組中,然後被忽略,此時 Timer 不會嘗試任何異常處理。

在這種情況下不會處理任何異常。上下文管理器的一大特點是,無論上下文如何退出,都會確保調用.__exit__()。在以下示例中,創建除零公式模擬異常查看代碼功能:

from timer import Timer
with Timer():
    for num in range(-3, 3):
        print(f"1 / {num} = {1 / num:.3f}")

1 / -3 = -0.333
1 / -2 = -0.500
1 / -1 = -1.000
Elapsed time: 0.0001 seconds
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
ZeroDivisionError: division by zero

註意 ,即使代碼拋出異常,Timer 也會打印出經過的時間。

使用 Python 定時器上下文管理器

現在我們將一起學習如何使用 Timer 上下文管理器來計時 "下載數據" 程序。回想一下之前是如何使用 Timer 的:

# download_data.py
import requests
from timer import Timer
def main():
    t = Timer()
    t.start()
    source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1'
    headers = {'User-Agent': 'Mozilla/5.0'}
    res = requests.get(source_url, headers=headers) 
    t.stop()
    with open('dataset/datasets.zip', 'wb') as f:
        f.write(res.content)

if __name__ == "__main__":
    main()

我們正在對 requests.get() 的調用進行記時監控。使用上下文管理器可以使代碼更短、更簡單、更易讀

# download_data.py
import requests
from timer import Timer
def main():
    source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1'
    headers = {'User-Agent': 'Mozilla/5.0'}
    with Timer():
        res = requests.get(source_url, headers=headers)
        
    with open('dataset/datasets.zip', 'wb') as f:
        f.write(res.content)

if __name__ == "__main__":
    main()

此代碼實際上與上面的代碼相同。主要區別在於沒有定義無關變量t,在命名空間上無多餘的東西。

寫在最後

將上下文管理器功能添加到 Python 計時器類有幾個優點:

  • 省時省力:隻需要一行額外的代碼即可為代碼塊的執行計時。
  • 可讀性高:調用上下文管理器是可讀的,你可以更清楚地可視化你正在計時的代碼塊。

使用 Timer 作為上下文管理器幾乎與直接使用 .start() 和 .stop() 一樣靈活,同時它的樣板代碼更少。

以上就是詳解利用上下文管理器擴展Python計時器的詳細內容,更多關於Python上下文管理器 計時器的資料請關註WalkonNet其它相關文章!

推薦閱讀: