資源的管理在程序的設計上是一個很常見的問題,例如管理檔案,開啟的網絡socket與各種鎖定(locks)等.最主要的問題在於我們必須確保這些開啟的資源在使用之后能夠關閉(或釋放),若忘記關閉這些資源,就會造成程序執行上的效能問題,嚴重的還會導致錯誤.除了關閉之外,一些特殊的資源上的管理要求在使用完畢后,還需要進行后續的清理工作,這些也是在資源管理上注意的.
python語言則提供了這么一種機制的語法操作,就是with.利用with,python的程序可以自動進行資源的建立,清理和回收動作,讓程序設計者可以更加方便的使用各種資源.
若不使用with語句,代碼如下:
1 file = open("filename") 2 data = file.read() 3 file.close
但是以上代碼存在兩個問題:
- 一是可能忘記關閉文件的句柄
- 二是文件讀取數據時若發生異常,造成程式提早離開,打開的資源就無法關閉
所以較好的程序寫法是下面的加強版本,try...,finally...
1 try: 2 # 打開文件 3 f = open("filename") 4 except: 5 print('fail to open') 6 exit(-1) 7 try: 8 do something 9 except: 10 do something 11 finally: 12 f.close()
這種方法雖然代碼運行良好,但是缺點就是過於冗長,切try與finally之間需要手動寫入代碼,不是很方便,也容易忘記.
這種情況下,我們就可以使用with的這種寫法:
1 # 以with打開文檔,並寫入Hello, python! 2 with open("filename", 'w') as f: 3 f.write('Hello, python!')
with如何工作?
- 緊跟with后面的語句被求值后,返回對象的'__enter__()'方法被調用,這個方法的返回值將被賦值給as后面的變量.
- 當with后面的代碼塊全部執行完之后,將被調用前面返回對象的__exit__()方法.
代碼示例1:
1 #/usr/bin/python 2 # -*- coding: utf-8 -*- 3 # 自定義 Context Manager 4 class File(object): 5 def __init__(self, filename, mode): 6 # 設定文本名與打開方式 7 self.filename = filename 8 self.mode = mode 9 10 # 資源配置 11 def __enter__(self): 12 print("打開文本:" + self.filename) 13 self.open_file = open(self.filename, self.mode) 14 return self.open_file 15 16 # 資源回收(關閉文本) 17 def __exit__(self, type, value, trace): 18 print("關閉文本:" + self.filename) 19 self.open_file.close()
使用方式:
1 with file open("filename", 'w') as f: 2 print("寫入文本...") 3 f.write("Hello, world!")
打開文本:filename
寫入文本...
關閉文本:filename
代碼示例2:
1 #!/usr/bin/env python3 2 class Sample: 3 def __enter__(self): 4 print("In __enter__()") 5 return "Foo" 6 7 def __exit__(self, type, value, trace): 8 print("In __exit__()") 9 10 def get_sample(): 11 return Sample() 12 13 with get_sample() as sample: 14 print("sample:", sample)
運行代碼,輸出如下:
1 In __enter__() 2 sample: Foo 3 In __exit__()
正如你看到的: 1. __enter__()方法被執行
2. __enter__()方法返回的值 - 這個例子中是”Foo”,賦值給變量’sample’
3. 執行代碼塊,打印變量”sample”的值為 “Foo”
4. __exit__()方法被調用 with真正強大之處是它可以處理異常。
可能你已經注意到Sample類的 __exit__ 方法有三個參數 val, type 和 trace。 這些參數在異常處理中相當有用。我們來改一下代碼,看看具體如何工作的。
1 class Sample: 2 def __enter__(self): 3 return self 4 def __exit__(self, type, value, trace): 5 print "type:", type 6 print "value:", value 7 print "trace:", trace 8 def do_something(self): 9 bar = 1/0 10 return bar + 10 11 with Sample() as sample: 12 sample.do_something()
這個例子中,with后面的get_sample()變成了Sample()。這沒有任何關系,只要緊跟with后面的語句所返回的對象有 __enter__() 和 __exit__() 方法即可。此例中,Sample()的 __enter__() 方法返回新創建的Sample對象,並賦值給變量sample。
代碼執行后:
1 type: <type 'exceptions.ZeroDivisionError'> 2 value: integer division or modulo by zero 3 trace: <traceback object at 0x1004a8128> 4 Traceback (most recent call last): 5 File "./with_example02.py", line 19, in <module> 6 sample.do_something() 7 File "./with_example02.py", line 15, in do_something 8 bar = 1/0 9 ZeroDivisionError: integer division or modulo by zero
實際上,在with后面的代碼塊拋出任何異常時,__exit__() 方法被執行。正如例子所示,異常拋出時,與之關聯的type,value和stack trace傳給 __exit__() 方法,因此拋出的ZeroDivisionError異常被打印出來了。開發庫時,清理資源,關閉文件等等操作,都可以放在 __exit__ 方法當中。
另外,__exit__ 除了用於tear things down,還可以進行異常的監控和處理,注意后幾個參數。要跳過一個異常,只需要返回該函數True即可。
下面的樣例代碼跳過了所有的TypeError,而讓其他異常正常拋出。
1 def __exit__(self, type, value, traceback): 2 return isinstance(value, TypeError)
上文說了 __exit__ 函數可以進行部分異常的處理,如果我們不在這個函數中處理異常,他會正常拋出,這時候我們可以這樣寫(python 2.7及以上版本,之前的版本參考使用contextlib.nested這個庫函數):
1 try: 2 with open( "a.txt" ) as f : 3 do something 4 except xxxError: 5 do something about exception
總之,with-as表達式極大的簡化了每次寫finally的工作,這對保持代碼的優雅性是有極大幫助的。
如果有多個項,我們可以這么寫:
1 with open("x.txt") as f1, open('xxx.txt') as f2: 2 do something with f1,f2
因此,Python的with語句是提供一個有效的機制,讓代碼更簡練,同時在異常產生時,清理工作更簡單。