上下文管理

上下文管理器

在使用Python編程中,能夠會常常碰到這種狀況:有一個特殊的語句塊,在執行這個語句塊以前須要先執行一些準備動做;當語句塊執行完成後,須要繼續執行一些收尾動做。數據庫

例如:當須要操做文件或數據庫的時候,首先須要獲取文件句柄或者數據庫鏈接對象,當執行完相應的操做後,須要執行釋放文件句柄或者關閉數據庫鏈接的動做。編程

又如,當多線程程序須要訪問臨界資源的時候,線程首先須要獲取互斥鎖,當執行完成並準備退出臨界區的時候,須要釋放互斥鎖。多線程

對於這些狀況,Python中提供了上下文管理器(Context Manager)的概念,能夠經過上下文管理器來定義/控制代碼塊執行前的準備動做,以及執行後的收尾動做。app

上下文管理協議

那麼在Python中怎麼實現一個上下文管理器呢?這裏,又要提到兩個"魔術方法",__enter__和__exit__,下面就是關於這兩個方法的具體介紹。函數

  • __enter__(self) Defines what the context manager should do at the beginning of the block created by the with statement. Note that the return value of __enter__ is bound to the target of the with statement, or the name after the as.
  • __exit__(self, exception_type, exception_value, traceback) Defines what the context manager should do after its block has been executed (or terminates). It can be used to handle exceptions, perform cleanup, or do something always done immediately after the action in the block. If the block executes successfully, exception_type, exception_value, and traceback will be None. Otherwise, you can choose to handle the exception or let the user handle it; if you want to handle it, make sure __exit__ returns True after all is said and done. If you don't want the exception to be handled by the context manager, just let it happen.

也就是說,當咱們須要建立一個上下文管理器類型的時候,就須要實現__enter__和__exit__方法,這對方法就稱爲上下文管理協議(Context Manager Protocol),定義了一種運行時上下文環境。post

with語句

在Python中,能夠經過with語句來方便的使用上下文管理器,with語句能夠在代碼塊運行前進入一個運行時上下文(執行__enter__方法),並在代碼塊結束後退出該上下文(執行__exit__方法)。ui

with語句的語法以下:this

with context_expr [as var]:
    with_suite
  • context_expr是支持上下文管理協議的對象,也就是上下文管理器對象,負責維護上下文環境
  • as var是一個可選部分,經過變量方式保存上下文管理器對象
  • with_suite就是須要放在上下文環境中執行的語句塊

在Python的內置類型中,不少類型都是支持上下文管理協議的,例如file,thread.LockType,threading.Lock等等。這裏咱們就以file類型爲例,看看with語句的使用。spa

with語句簡化文件操做

當須要寫一個文件的時候,通常都會經過下面的方式。代碼中使用了try-finally語句塊,即便出現異常,也能保證關閉文件句柄。線程

複製代碼
logger = open("log.txt", "w")
try:
    logger.write('Hello ')
    logger.write('World')
finally:
    logger.close()

print logger.closed

複製代碼

其實,Python的內置file類型是支持上下文管理協議的,能夠直接經過內建函數dir()來查看file支持的方法和屬性:

複製代碼
>>> print dir(file)
['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '
__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclass
hook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', '
mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines',
'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']
>>>
複製代碼

因此,能夠經過with語句來簡化上面的代碼,代碼的效果是同樣的,可是使用with語句的代碼更加的簡潔:

with open("log.txt", "w") as logger:
    logger.write('Hello ')
    logger.write('World')

print logger.closed

自定義上下文管理器

對於自定義的類型,能夠經過實現__enter__和__exit__方法來實現上下文管理器。

看下面的代碼,代碼中定義了一個MyTimer類型,這個上下文管理器能夠實現代碼塊的計時功能:

複製代碼
import time

class MyTimer(object):
def init(self, verbose = False):
self.verbose
= verbose



    
    
    
    
複製代碼
</span><span style="color: #0000ff;">def</span> <span style="color: #800080;">__enter__</span><span style="color: #000000;">(self): self.start </span>=<span style="color: #000000;"> time.time() </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> self </span><span style="color: #0000ff;">def</span> <span style="color: #800080;">__exit__</span>(self, *<span style="color: #000000;">unused): self.end </span>=<span style="color: #000000;"> time.time() self.secs </span>= self.end -<span style="color: #000000;"> self.start self.msecs </span>= self.secs * 1000 <span style="color: #0000ff;">if</span><span style="color: #000000;"> self.verbose: </span><span style="color: #0000ff;">print</span> <span style="color: #800000;">"</span><span style="color: #800000;">elapsed time: %f ms</span><span style="color: #800000;">"</span> %self.msecs</pre>

下面結合with語句使用這個上下文管理器:

複製代碼
def fib(n):
    if n in [1, 2]:
        return 1
    else:
        return fib(n-1) + fib(n-2)

with MyTimer(True):
print fib(30)

複製代碼

代碼輸出結果爲:

異常處理和__exit__

在使用上下文管理器中,若是代碼塊 (with_suite)產生了異常,__exit__方法將被調用,而__exit__方法又會有不一樣的異常處理方式。

當__exit__方法退出當前運行時上下文時,會並返回一個布爾值,該布爾值代表了"若是代碼塊 (with_suite)執行中產生了異常,該異常是否需要被忽略"。

1. __exit__返回False,從新拋出(re-raised)異常到上層

修改前面的例子,在MyTimer類型中加入了一個參數"ignoreException"來表示上下文管理器是否會忽略代碼塊 (with_suite)中產生的異常。

複製代碼
import time

class MyTimer(object):
def init(self, verbose = False, ignoreException = False):
self.verbose
= verbose
self.ignoreException
= ignoreException


try:
with MyTimer(True, False):
raise Exception("Ex4Test")
except Exception, e:
print "Exception (%s) was caught" %e
else:
print "No Exception happened"

</span><span style="color: #0000ff;">def</span> <span style="color: #800080;">__enter__</span><span style="color: #000000;">(self): self.start </span>=<span style="color: #000000;"> time.time() </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> self </span><span style="color: #0000ff;">def</span> <span style="color: #800080;">__exit__</span>(self, *<span style="color: #000000;">unused): self.end </span>=<span style="color: #000000;"> time.time() self.secs </span>= self.end -<span style="color: #000000;"> self.start self.msecs </span>= self.secs * 1000 <span style="color: #0000ff;">if</span><span style="color: #000000;"> self.verbose: </span><span style="color: #0000ff;">print</span> <span style="color: #800000;">"</span><span style="color: #800000;">elapsed time: %f ms</span><span style="color: #800000;">"</span> %<span style="color: #000000;">self.msecs </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> self.ignoreException

複製代碼

運行這段代碼,會獲得如下結果,因爲__exit__方法返回False,因此代碼塊 (with_suite)中的異常會被繼續拋到上層代碼。

2. __exit__返回Ture,代碼塊 (with_suite)中的異常被忽略

將代碼改成__exit__返回爲True的狀況:

複製代碼
try:        
    with MyTimer(True, True):
        raise Exception("Ex4Test")
except Exception, e:
    print "Exception (%s) was caught" %e
else:
    print "No Exception happened"
複製代碼

運行結果就變成下面的狀況,代碼塊 (with_suite)中的異常被忽略了,代碼繼續運行:

必定要當心使用__exit__返回Ture的狀況,除非很清楚爲何這麼作。

3. 經過__exit__函數完整的簽名獲取更多異常信息

對於__exit__函數,它的完整簽名以下,也就是說經過這個函數能夠得到更多異常相關的信息。

  • __exit__(self, exception_type, exception_value, traceback)

繼續修改上面例子中的__exit__函數以下:

複製代碼
def __exit__(self, exception_type, exception_value, traceback):
    self.end = time.time()
    self.secs = self.end - self.start
    self.msecs = self.secs * 1000
    if self.verbose:
        print "elapsed time: %f ms" %self.msecs

return self.ignoreException

</span><span style="color: #0000ff;">print</span> <span style="color: #800000;">"</span><span style="color: #800000;">exception_type: </span><span style="color: #800000;">"</span><span style="color: #000000;">, exception_type </span><span style="color: #0000ff;">print</span> <span style="color: #800000;">"</span><span style="color: #800000;">exception_value: </span><span style="color: #800000;">"</span><span style="color: #000000;">, exception_value </span><span style="color: #0000ff;">print</span> <span style="color: #800000;">"</span><span style="color: #800000;">traceback: </span><span style="color: #800000;">"</span><span style="color: #000000;">, traceback

複製代碼

此次運行結果中,就顯示出了更多異常相關的信息了:

總結

本文介紹了Python中的上下文管理器,以及如何結合with語句來使用上下文管理器。

總結一下with 語句的執行流程:

  • 執行context_expr 以獲取上下文管理器對象
  • 調用上下文管理器的 __enter__() 方法
    • 若是有 as var 從句,則將 __enter__() 方法的返回值賦給 var
  • 執行代碼塊 with_suite
  • 調用上下文管理器的 __exit__() 方法,若是 with_suite 產生異常,那麼該異常的 type、value 和 traceback 會做爲參數傳給 __exit__(),不然傳三個 None
    • 若是 with_suite 產生異常,而且 __exit__() 的返回值等於 False,那麼這個異常將被從新拋出到上層
    • 若是 with_suite 產生異常,兵器 __exit__() 的返回值等於 True,那麼這個異常就被忽略,繼續執行後面的代碼

在不少狀況下,with語句能夠簡化代碼,並增長代碼的健壯性。

相關文章
相關標籤/搜索