PyTips 0x0d - Python 上下文管理器

項目地址:https://git.io/pytipshtml

Python 2.5 引入了 with 語句(PEP 343)與上下文管理器類型(Context Manager Types),其主要做用包括:python

保存、重置各類全局狀態,鎖住或解鎖資源,關閉打開的文件等。With Statement Context Managersgit

一種最廣泛的用法是對文件的操做:github

with open("utf8.txt", "r") as f:
    print(f.read())
你好,世界!

上面的例子也能夠用 try...finally... 實現,它們的效果是相同(或者說上下文管理器就是封裝、簡化了錯誤捕捉的過程):spa

try:
    f = open("utf8.txt", "r")
    print(f.read())
finally:
    f.close()
你好,世界!

除了文件對象以外,咱們也能夠本身建立上下文管理器,與 0x01 中介紹的迭代器相似,只要定義了 __enter__()__exit__() 方法就成爲了上下文管理器類型。with 語句的執行過程以下:code

  1. 執行 with 後的語句獲取上下文管理器,例如 open('utf8.txt', 'r') 就是返回一個 file objectorm

  2. 加載 __exit__() 方法備用;htm

  3. 執行 __enter__(),該方法的返回值將傳遞給 as 後的變量(若是有的話);對象

  4. 執行 with 語法塊的子句;ip

  5. 執行 __exit__() 方法,若是 with 語法塊子句中出現異常,將會傳遞 type, value, traceback__exit__(),不然將默認爲 None;若是 __exit__() 方法返回 False,將會拋出異常給外層處理;若是返回 True,則忽略異常。

瞭解了 with 語句的執行過程,咱們能夠編寫本身的上下文管理器。假設咱們須要一個引用計數器,而出於某些特殊的緣由須要多個計數器共享全局狀態而且能夠相互影響,並且在計數器使用完畢以後須要恢復初始的全局狀態:

_G = {"counter": 99, "user": "admin"}

class Refs():
    def __init__(self, name = None):
        self.name = name
        self._G = _G
        self.init = self._G['counter']
    def __enter__(self):
        return self
    def __exit__(self, *args):
        self._G["counter"] = self.init
        return False
    def acc(self, n = 1):
        self._G["counter"] += n
    def dec(self, n = 1):
        self._G["counter"] -= n
    def __str__(self):
        return "COUNTER #{name}: {counter}".format(**self._G, name=self.name)
        
with Refs("ref1") as ref1, Refs("ref2") as ref2: # Python 3.1 加入了多個並列上下文管理器
    for _ in range(3):
        ref1.dec()
        print(ref1)
        ref2.acc(2)
        print(ref2)
print(_G)
COUNTER #ref1: 98
COUNTER #ref2: 100
COUNTER #ref1: 99
COUNTER #ref2: 101
COUNTER #ref1: 100
COUNTER #ref2: 102
{'user': 'admin', 'counter': 99}

上面的例子很彆扭可是能夠很好地說明 with 語句的執行順序,只是每次定義兩個方法看起來並非很簡潔,一如既往地,Python 提供了 @contextlib.contextmanager + generator 的方式來簡化這一過程(正如 0x01yield 簡化迭代器同樣):

from contextlib import contextmanager as cm
_G = {"counter": 99, "user": "admin"}

@cm
def ref():
    counter = _G["counter"]
    yield _G
    _G["counter"] = counter

with ref() as r1, ref() as r2:
    for _ in range(3):
        r1["counter"] -= 1
        print("COUNTER #ref1: {}".format(_G["counter"]))
        r2["counter"] += 2
        print("COUNTER #ref2: {}".format(_G["counter"]))
print("*"*20)
print(_G)
COUNTER #ref1: 98
COUNTER #ref2: 100
COUNTER #ref1: 99
COUNTER #ref2: 101
COUNTER #ref1: 100
COUNTER #ref2: 102
********************
{'user': 'admin', 'counter': 99}

這裏對生成器的要求是必須只能返回一個值(只有一次 yield),返回的值至關於 __enter__() 的返回值;而 yield 後的語句至關於 __exit__()

生成器的寫法更簡潔,適合快速生成一個簡單的上下文管理器。

除了上面兩種方式,Python 3.2 中新增了 contextlib.ContextDecorator,能夠容許咱們本身在 class 層面定義新的」上下文管理修飾器「,有興趣能夠到官方文檔查看。至少在我目前看來好像並無帶來更多方便(除了能夠省掉一層縮進以外:()。

上下文管理器的概念與修飾器有不少類似之處,可是要記住的是 with 語句的目的是爲了更優雅地收拾殘局而不是替代 try...finally...,畢竟在 The Zen of Python 中,

Explicit is better than implicit.

Simple is better than complex.

更重要:P。


歡迎關注公衆號 PyHub!

歡迎關注公衆號 PyHub!

相關文章
相關標籤/搜索