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

執行過程:

  1. 執行上下文表達式(context_expr)以獲得上下文管理器對象。
  2. 加載上下文管理器對象的__exit__()方法,備用。
  3. 執行上下文管理器對象的__enter__()方法。
  4. 如果有as var語句,將__enter__()方法返回值綁定到 as 後面的 變量中。
  5. 執行 with 內的代碼塊(with_body)。
  6. 執行上下文管理器的__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>

可以看出,輸出的流程:

  1. 先輸出yield前的輸出語句;
  2. 然後再是tag()函數的輸出語句,
  3. 最後是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()