Python 上下文(Context)學
- 2020 年 1 月 17 日
- 筆記
前言
最早接觸到with
語句的時候,是初學python,對文件進行讀寫的時候,當時文件讀寫一般都是用open()函數來對文件進行讀寫,為了防止讀寫的過程中出現錯誤,也為了讓代碼更加的pythonic,會接觸到with
語句
with open('file.text', 'w') as f: f.write('hello')
上面的代碼僅需兩行就實現了對文件進行寫入的操作,很方便,代碼也更整潔,不會出錯。
事實上,上面一段代碼就用到了上下文管理器的知識。
某種程度上,上下文管理器可以理解成try/finally的優化,使得代碼更加易讀,在通常情況下,我們讀取文件的時候,如果不適用with語句,為了防止出錯,可以採用try/finally的語句來進行讀取,使得文件可以正常執行close()方法。
f = open('file.text', 'w'): try: f.write('hello') finally: f.close()
很明顯,with語句比try/finally更易讀,更友好。
上下文管理器
上下文管理器協議,是指要實現對象的 __enter__()
和 __exit__()
方法。
上下文管理器也就是支持上下文管理器協議的對象,也就是實現了 __enter__()
和 __exit__()
方法。
上下文管理器 是一個對象,它定義了在執行 with
語句時要建立的運行時上下文。 上下文管理器處理進入和退出所需運行時上下文以執行代碼塊。 通常使用 with
語句來使用,但是也可以通過直接調用它們的方法來使用。
簡單來說,我們定義一個上下文管理器,需要在一個類裏面一個實現__enter__(self)
和 __exit__(self, exc_type, exc_value, traceback)
方法。
-
object.__enter__(self)
進入與此對象相關的運行時上下文,並返回自身或者另一個與運行食上下文相關的對象。(with語句將會綁定這個方法的返回值到as
子句中指定的目標) -
object.__exit__(self, exc_type, exc_value, traceback)
退出關聯到此對象的運行時上下文。 各個參數描述了導致上下文退出的異常。 如果上下文是無異常地退出的,三個參數都將為None。如果提供了異常,並且希望方法屏蔽此異常(即避免其被傳播),則應當返回真值。 否則的話,異常將在退出此方法時按正常流程處理。請注意__exit__()
方法不應該重新引發被傳入的異常,這是調用者的責任。如果 with_body 的退出由異常引發,並且__exit__()
的返回值等於 False,那麼這個異常將被重新引發一次;如果__exit__()
的返回值等於 True,那麼這個異常就被無視掉,繼續執行後面的代碼。
通常情況下,我們會使用with語句來使用上下文管理器:
with context_expr [as var]: with_body
執行過程:
- 執行上下文表達式(context_expr)以獲得上下文管理器對象。
- 加載上下文管理器對象的
__exit__()
方法,備用。 - 執行上下文管理器對象的
__enter__()
方法。 - 如果有
as var
語句,將__enter__()
方法返回值綁定到 as 後面的 變量中。 - 執行 with 內的代碼塊(with_body)。
- 執行上下文管理器的__exit__()方法。
把文章開頭的例子用上下文管理器實現一邊:
class OpenFile(object): def __init__(self, filename): self.file = open(filename, 'w+') def __enter__(self): return self.file def __exit__(self, exc_type, exc_val, exc_tb): self.file.close() def main(): with OpenFile('text.txt') as f: f.write('ok') if __name__ == "__main__": main()
總結:在上下文管理器中,生成類實例的時候,會自動調用__enter__()
方法,而在結束的時候,會自動調用__exit__()
方法。
所以,在定義上下文管理器的時候,我們只需實現好這兩個方法就行了。
上下文管理器的運用場景
上下文管理器的典型用法包括保存和恢復各種全局狀態,鎖定和解鎖資源,關閉打開的文件等。
比如我們需要在一段代碼中使用到數據庫的查詢,可以通過上下文處理器來優化我們的代碼結構,
contextilb模塊
contextilb模塊是python內置模塊中的一個用於上下文的模塊,可以讓我們更優雅地使用上下文管理器。
@contextmanager
這是contextlib模塊提供的一個裝飾器,用於將一個函數聲明上下文管理,無需創建一個類或者單獨的__enter__()
方法和__exit__()
方法,就可以實現上下文管理。
需要注意的是,被裝飾的函數被調用的時候必須返回一個生成器,而且這個生成器只生成一個值,如果有as的話,該值講綁定到with語句as子句的目標中。
from contextlib import contextmanager @contextmanager def tag(name): print('<{}>'.format(name)) yield print('</{}>'.format(name)) with tag('title'): print("This is a contextmanger test")
輸出為:
<title> This is a contextmanger test </title>
可以看出,輸出的流程:
- 先輸出
yield
前的輸出語句; - 然後再是
tag()
函數的輸出語句, - 最後是
yield
後面的輸出語句。
在生成器函數中的yield之前的語句在__enter__()
方法中執行,
相當於
def __enter__(self): print('<{}>'.format(name)) def __exit__(self, exc_type, exc_val, exc_tb): print('</{}>'.format(name))
closing
返回一個上下文管理器,在完成代碼塊的時候會關閉參數
源碼參考:
class closing(AbstractContextManager): def __init__(self, thing): self.thing = thing def __enter__(self): return self.thing def __exit__(self, *exc_info): self.thing.close()
常見用法,如寫爬蟲的時候,可以這樣寫:
from contextlib import closing import requests url = 'http://www.baidu.com' with closing(requests.get(url)) as page: for line in page: print(page)
上下文管理器查詢數據庫
代碼:
import pymysql class Database(object): def __init__(self): self.db = pymysql.connect("localhost", "root", "root", "test") self.cursor = self.db.cursor() def query(self, sql): self.cursor.execute(sql) result = self.cursor.fetchone() return result def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.cursor.close() self.db.close() def main(): sql = "SELECT password FROM USER WHERE username='{}' ORDER BY 1;".format('admin') with Database() as s: a = s.query(sql) print(a) if __name__ == "__main__": main()
使用contextlib模塊編寫:
class Database(object): def __init__(self): self.db = pymysql.connect("localhost", "root", "root", "test") self.cursor = self.db.cursor() def query(self, sql): self.cursor.execute(sql) result = self.cursor.fetchone() return result @contextmanager def database_query(): q = Database() yield q def main(): sql = "SELECT password FROM USER WHERE username='{}' ORDER BY 1;".format('admin') with database_query() as s: a = s.query(sql) print(a) if __name__ == "__main__": main()