一般來講,實現上下文管理器,須要編寫一個帶有__enter__和 __exit__的類,相似這樣:python
class ListTransaction: def __init__(self, orig_list): self.orig_list = orig_list self.working = list(orig_list) def __enter__(self): return self.working def __exit__(self, exc_type, exc_val, exc_tb): self.orig_list[:] = self.working
然而,在contextlib模塊中,還提供了@contextmanager裝飾器,將一個生成器函數當成上下文管理器使用,上面的代碼在大部分,是與下面的代碼等效的。git
本文的list_transaction函數的代碼來自:《Python Cookbook》 9.22 以簡單的方式定義上下文管理器github
from contextlib import contextmanager
@contextmanager def list_transaction(orig_list): working = list(orig_list) yield working orig_list[:] = working
先逐一分析上面的代碼:app
測試代碼以下:less
當執行過程當中,沒有引起異常時,執行正常,輸出 [1, 2, 3, 4, 5]ide
items_1 = [1, 2, 3] with list_transaction(items_1) as working_1: working_1.append(4) working_1.append(5) print(items_1)
當執行過程當中,引起異常時,yield後的代碼不會執行,orig_list不會被修改。從而實現事務的效果,orig_list還是 [1, 2, 3]函數
items_2 = [1, 2, 3] try: with list_transaction(items_2) as working_2: working_2.append(4) working_2.append(5) raise RuntimeError('oops') except Exception as ex: print(ex) finally: print(items_2)
上下文管理器類與@contextmanager中最大的區別在於對異常的處理。oop
分析contextmanager的源碼可知,@contextmanager裝飾器的本質是實例化一個_GeneratorContextManager對象。測試
def contextmanager(func): @wraps(func) def helper(*args, **kwds): return _GeneratorContextManager(func, args, kwds) return helper
進一步查看_GeneratorContextManager源碼,可知_GeneratorContextManager實現的是一個上下文管理器對象this
class _GeneratorContextManager(ContextDecorator): """Helper for @contextmanager decorator.""" def __init__(self, func, args, kwds): self.gen = func(*args, **kwds) self.func, self.args, self.kwds = func, args, kwds # Issue 19330: ensure context manager instances have good docstrings doc = getattr(func, "__doc__", None) if doc is None: doc = type(self).__doc__ self.__doc__ = doc # Unfortunately, this still doesn't provide good help output when # inspecting the created context manager instances, since pydoc # currently bypasses the instance docstring and shows the docstring # for the class instead. # See http://bugs.python.org/issue19404 for more details. def _recreate_cm(self): # _GCM instances are one-shot context managers, so the # CM must be recreated each time a decorated function is # called return self.__class__(self.func, self.args, self.kwds) def __enter__(self): try: return next(self.gen) except StopIteration: raise RuntimeError("generator didn't yield") from None def __exit__(self, type, value, traceback): if type is None: try: next(self.gen) except StopIteration: return else: raise RuntimeError("generator didn't stop") else: if value is None: # Need to force instantiation so we can reliably # tell if we get the same exception back value = type() try: self.gen.throw(type, value, traceback) raise RuntimeError("generator didn't stop after throw()") except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration # raised inside the "with" statement from being suppressed. return exc is not value except RuntimeError as exc: # Likewise, avoid suppressing if a StopIteration exception # was passed to throw() and later wrapped into a RuntimeError # (see PEP 479). if exc.__cause__ is value: return False raise except: # only re-raise if it's *not* the exception that was # passed to throw(), because __exit__() must not raise # an exception unless __exit__() itself failed. But throw() # has to raise the exception to signal propagation, so this # fixes the impedance mismatch between the throw() protocol # and the __exit__() protocol. # if sys.exc_info()[1] is not value: raise
簡要分析實現的代碼:
__enter__方法:
__exit__方法:
正常執行的狀況:
出現異常的狀況:
因此,以類的方式實現的上下文管理器,在引起異常時,__exit__方法內的代碼仍會正常執行;
而以生成器函數實現的上下文管理器,在引起異常時,__exit__方法會將異常傳遞給生成器,若是生成器沒法正確處理異常,則yield以後的代碼不會執行。
因此,大部分狀況下,yield都必須在try...except中,除非設計之初就是讓yield以後的代碼在with代碼塊內部出現異常時不執行。
測試代碼:
以類的方式實現上下文管理器,當沒有引起異常時, # 其執行結果與@contextmanager裝飾器裝飾器的上下文管理器函數相同,輸出 [1, 2, 3, 4, 5]
items_3 = [1, 2, 3] with ListTransaction(items_3) as working_3: working_3.append(4) working_3.append(5) print(items_3)
當執行代碼過程當中引起異常時,即便沒有對異常進行任何處理,__exit__方法也會正常執行,對self.orig_list進行修改(python是引用傳值,而list是可變類型,對orig_list的任何引用的修改,都會改變orig_list的值),因此輸出結果與沒有引起異常時相同:[1, 2, 3, 4, 5]
items_4 = [1, 2, 3] try: with ListTransaction(items_4) as working_4: working_4.append(4) working_4.append(5) raise RuntimeError('oops') except Exception as ex: print(ex) finally: print(items_4)
完整代碼:https://github.com/blackmatrix7/python-learning/blob/master/class_/contextlib_.py