python 基礎知識梳理——Python中的上下文管理器
python 基礎知識梳理——上下文管理器
1. 引言
在Python中,我們難免會操作檔案、連線斷開資料庫,這些都是很常見的操作,在檔案操作的時候,我們必須保證開啟檔案後呼叫close()
,連線資料庫後也需要close()
操作,不然就容易造成資源洩露,輕則系統處理緩慢,重則系統崩潰。
老規矩,我們從一個例子入手:
for x in range(10000000):
f =open('test.txt','w')
f.write('hello')
很明顯,這樣的瘋狂的行為會導致錯誤,因為我們一共打開了1000萬次檔案而沒有關閉,佔用了太多資源,造成了系統崩潰。
為了解決這個問題,Python中引用了上下文管理器(context manager),上下文管理器能夠幫助你自動分配並且釋放資源,其中最經典的就是with
語句,對於上面的例子,我們一般是使用上下文管理器進行檔案操作的:
for x in range(10000000):
with open('test.txt','w') as f:
f.write('hello')
這樣使用的話,當每次開啟檔案之後,Python就會幫我們自動關閉檔案,相應的資源也可以得到釋放,當然我們也可以改寫為:
f = open('test.txt','w')
try:
f.write('hello' )
finally:
f.close()
其中,我們還可以通過try/except/finally
捕獲異常,不過相對於with
來說,這樣的程式碼就顯得很冗餘。
2. 上下文管理器的實現
2.1 基於類的上下文管理器
我們通過一個例子,搞清楚上下文管理器的原理,搞清楚它的內部實現。在下面的例子中,我們手動實現一個上下文管理器FileSystem,手動定義它的開啟關閉操作。
class FileSystem:
def __init__(self, name, mode):
print('呼叫init方法')
self.name = name
self. mode = mode
self.file = None
def __enter__(self): # 定義enter方法
print('呼叫enter方法')
self.file = open(self.name, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb): # 定義exit方法
print('呼叫exit方法')
if self.file:
self.file.close()
with FileSystem('text.txt', 'w') as f:
print('準備寫入檔案')
f.write('hello world')
# 輸出
呼叫init方法
呼叫enter方法
準備寫入檔案
呼叫exit方法
從我們實現的上下文管理器可以看出,呼叫的步驟為:
- 首先呼叫
__init__
方法,初始化物件FileSystem
,此時傳入的檔名為test.txt
,模式為'w'
- 方法
__enter__
被呼叫,檔案test.txt
以寫入模式開啟,並且返回FileSystem
物件賦予變數f- 字串
'hello world'
被寫入檔案- 方法
__exit__
被呼叫,關閉之前開啟的檔案流
其中的__exit__
方法中的引數exc_type, exc_val, exc_tb
,分別表示exception_type、exception_value、exception_traceback。當我們使用with
語句時候,如果發生異常,那麼異常資訊就會包括在這三個變數中,傳入方法__exit__()
中。
下面我們在__exit__()
中新增捕獲異常。
class Error:
def __init__(self):
print('呼叫__init__')
def __enter__(self):
print('呼叫__enter__ ')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('呼叫__exit__')
if exc_type:
print(f'exception_type:{exc_type}')
print(f'exception_value:{exc_val}')
print(f'exception_traceback:{exc_tb}')
return True
with Error() as obj:
raise Exception('exception raised').with_traceback(None)
# 輸出
呼叫__init__
呼叫__enter__
呼叫__exit__
exception_type:<class 'Exception'>
exception_value:exception raised
exception_traceback:<traceback object at 0x7fe0a72c5a00>
2.2 簡單的基於上下文管理器的資料庫連線
class DBConnectionManager:
def __init__(self, hostname, port):
self.hostname = hostname
self.port = port
self.connection = None
def __enter__(self):
self.connection = DBClient(self.hostname, self.port)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.connection.close()
with DBConnectionManager('127.0.0.1', '8080') as dbclient:
pass
2.3 基於生成器的上下文管理器
基於類的上下文管理器是我們最常見也是最常用的形式,不過Python中的上下文管理器除了可以基於類實現,還可以基於生成器實現。
比如,我們可以通過裝飾器contextlib.contextmanager
,來定義自己所需的基於生成器的上下文管理器,也可以支援with語句,我們通過例子來實現:
from contextlib import contextmanager
@contextmanager
def file_manager(name, mode):
try:
f = open(name, mode)
yield f
finally:
f.close()
with file_manager('test.txt', 'w') as f:
f.write('hello world')
在這段程式碼中,file_manager()
是一個生成器,當我們執行with語句的時候,便會開啟執行檔案,並返回檔案物件f,當with語句執行完成後,finnally block 中的關閉檔案操作便會執行。
當我們使用基於生成器的上下文管理器時,我們就不用再定義
__enter__()
方法和__exit__()
方法了,但是必須記得加上@contextmanager
基於類的上下文管理器和基於生成器的上下文管理器在功能上是一致的,但是:
- 基於類的上下文管理器更靈活,適用於大型系統的開發;
- 基於生成器的上下文管理器更加方便、簡潔,適用於中小型程式開發;
3. 總結
上下文管理器一共分為兩種:①基於類的上下文管理器②基於生成器的上下文管理器
具體的使用哪種上下文管理器,要結合使用場景。上下文管理器通常和with語句一起使用,大大提高了程式的簡潔程度,上下文管理器一般都包括enter、exit
這兩個部分,一旦有異常丟擲,異常的型別、異常的值、異常的回溯物件等資訊都會通過引數傳入__exit__()
函式中,我們可以自定義相關操作對異常進行處理。
博文的後續更新,請關注我的個人部落格:星塵部落格