天天看點

【Python學習筆記】with語句與上下文管理器with語句上下文管理器contextlib子產品

  • with語句
  • 上下文管理器
  • contextlib子產品
  • 參考

with語句

with語句時在Python2.6中出現的新語句。在Python2.6以前,要正确的處理涉及到異常的資源管理時,需要使用try/finally代碼結構。如要實作檔案在操作出現異常時也能正确關閉,則需要像如下實作:

f = open("test.txt")
try:
    for line in f.readlines():
        print(line)
finally:
    f.close()
           

不管檔案操作有沒有出現異常,try/finally中的finnally語句都會執行,進而保證檔案的正确關閉。但是很顯然Python的設計者們并沒有滿足于此,他們以希望更簡潔更優美的形式來實作資源的清理,而且希望這種清理工作不需要暴露給使用者,是以便出現了with語句。

with語句的基本文法結構如下:

with expression [as variable]:
    with-block
           

先看下如果用with語句代替上面的try/finally的例子,然後再讨論它的更多細節,如下:

with open("text.txt") as f:
    for line in f.readlines()
    print(line)
           

是不是發現使用with語句相對try/finally來說簡潔了很多,而且也不需要每一個使用者都去寫f.close()來關閉檔案了,這是因為with語句在背後做了大量的工作。with語句的expression是上下文管理器,這個我們下文會說。with語句中的[as variable]是可選的,如果指定了as variable說明符,則variable是上下文管理器expression調用__enter__()函數傳回的對象。是以,f并不一定就是expression,而是expression.enter()的傳回值,至于expression.enter()傳回什麼就由這個函數來決定了。with-block是執行語句,with-block執行完畢時,with語句會自動進行資源清理,對應上面例子就是with語句會自動關閉檔案。

下面我們來具體說下with語句在背後默默無聞地到底做了哪些事情。剛才我們說了expression是一個上下文管理器,其實作了__enter__和__exit__兩個函數。當我們調用一個with語句時,執行過程如下:

1.首先生成一個上下文管理器expression,在上面例子中with語句首先以“test.txt”作為參數生成一個上下文管理器open(“test.txt”)。

2.然後執行expression.enter()。如果指定了[as variable]說明符,将__enter__()的傳回值賦給variable。上例中open(“test.txt”).enter()傳回的是一個檔案對象給f。

3.執行with-block語句塊。上例中執行讀取檔案。

4.執行expression.exit(),在__exit__()函數中可以進行資源清理工作。上面例子中就是執行檔案的關閉操作。

with語句不僅可以管理檔案,還可以管理鎖、連接配接等等,如下面的例子:

#管理鎖
import  threading
lock = threading.lock()
with lock:
    #執行一些操作
    pass
           

上下文管理器

在上文中我們提到with語句中的上下文管理器。with語句可以如此簡單但強大,主要依賴于上下文管理器。那麼什麼是上下文管理器?上下文管理器就是實作了上下文協定的類,而上下文協定就是一個類要實作__enter__()和__exit__()兩個方法。一個類隻要實作了__enter__()和__exit__(),我們就稱之為上下文管理器下面我們具體說下這兩個方法。

enter():主要執行一些環境準備工作,同時傳回一資源對象。如果上下文管理器open(“test.txt”)的__enter__()函數傳回一個檔案對象。

exit():完整形式為__exit__(type, value, traceback),這三個參數和調用sys.exec_info()函數傳回值是一樣的,分别為異常類型、異常資訊和堆棧。如果執行體語句沒有引發異常,則這三個參數均被設為None。否則,它們将包含上下文的異常資訊。exit()方法傳回True或False,分别訓示被引發的異常有沒有被處理,如果傳回False,引發的異常将會被傳遞出上下文。如果__exit_()函數内部引發了異常,則會覆寫掉執行體的中引發的異常。處理異常時,不需要重新抛出異常,隻需要傳回False,with語句會檢測__exit__()傳回False來處理異常。

如果我們要自定義一個上下文管理器,隻需要定義一個類并且是實作__enter__()和__exit__()即可。下面通過一個簡單的例子是示範如果建立自定義的上下文管理器,我們以資料庫的連接配接為例。在使用資料庫時,有時要涉及到事務操作。資料庫的事務操作當調用commit()執行sql指令時,如果在這個過程中執行失敗,則需要執行rollback()復原資料庫,通常實作方式可能如下:

def test_write():
    con = MySQLdb.connection()
    cursor = con.cursor()
    sql = """      #具體的sql語句
    """
    try:
        cursor.execute(sql)
        cursor.execute(sql)
        cursor.execute(sql)
        con.commit()      #送出事務
    except Exception as ex:
        con.rollback()    #事務執行失敗,復原資料庫
           

如果想通過with語句來實作資料庫執行失敗的復原操作,則我們需要自定義一個資料庫連接配接的上下文管理器,假設為DBConnection,則我們将上面例子用with語句來實作的話,應該是這樣子的,如下:

def test_write():
    sql = """      #具體的sql語句
    """
    con = DBConnection()
    with con as cursor:   
        cursor.execute(sql)
        cursor.execute(sql)
        cursor.execute(sql)
           

要實作上面with語句的功能,則我們的DBConnection資料庫上下文管理器則需要提供一下功能:enter()要傳回一個連接配接的cursor; 當沒有異常發生是,exit()函數commit所有的資料庫操作。如果有異常發生則_exit__()會復原資料庫,調用rollback()。是以我們可以實作DBConnection如下:

def DBConnection(object):
    def __init__(self):
        pass

    def cursor(self):
        #傳回一個遊标并且啟動一個事務
        pass

    def commit(self):
        #送出目前事務
        pass

    def rollback(self):
        #復原目前事務
        pass

    def __enter__(self):
        #傳回一個cursor
        cursor = self.cursor()
        return cursor

    def __exit__(self, type, value, tb):
        if tb is None:
            #沒有異常則送出事務
            self.commit() 
        else:
            #有異常則復原資料庫
            self.rollback()
           

contextlib子產品

contextmanage對象

上文提到如果我們要實作一個自定義的上下文管理器,需要定義一個實作了__enter__和__exit__兩個方法的類, 這顯示不是很友善。Python的contextlib子產品給我們提供了更友善的方式來實作一個自定義的上下文管理器。contextlib子產品包含一個裝飾器contextmanager和一些輔助函數,裝飾器contextmanager隻需要寫一個生成器函數就可以代替自定義的上下文管理器,典型用法如下:

需要使用yield先定義一個生成器函數:

@contextmanager
        def some_generator(<arguments>):
            <setup>
            try:
                yield <value>
            finally:
                <cleanup>
           

然後便可以用with語句調用contextmanage生成的上下文管理器了,with語句用法如下:

with some_generator(<arguments>) as <variable>:
            <body>
           

生成器函數some_generator就和我們普通的函數一樣,它的原理如下:

  1. some_generator函數在在yield之前的代碼等同于上下文管理器中的__enter__函數。
  2. yield的傳回值等同于__enter__函數的傳回值,即如果with語句聲明了as

    ,則yield的值會賦給variable

  3. 然後執行代碼塊,等同于上下文管理器的__exit__函數。此時發生的任何異常都會再次通過yield函數傳回。

    下面舉幾個簡單的例子:

    例子1:鎖資源自動擷取和釋放的例子

@contextmanager
def locked(lock):
    lock.acquire()
    try:
        yield
    finally:
        lock.release()

with locked(myLock):
    #代碼執行到這裡時,myLock已經自動上鎖
    pass
    #執行完後會,會自動釋放鎖
           

例子2:檔案打開後自動管理的實作

@contextmanager
def myopen(filename, mode="r"):
    f = open(filename,mode)
    try:
        yield f
    finally:
        f.close()

with myopen("test.txt") as f:
    for line in f:
        print(line)
           

例子3:資料庫事務的處理:

@contextmanager
def transaction(db):
    db.begin()
    try:
        yield 
    except:
        db.rollback()
        raise
    else:
        db.commit()

with transaction(mydb):
    mydb.cursor.execute(sql)
    mydb.cursor.execute(sql)
    mydb.cursor.execute(sql)
    mydb.cursor.execute(sql)
           

nested函數

contextlib子產品還提供了一個函數給我們:nested(mgr1,mgr2…mgrn)函數,用來嵌套多個上下文管理器,等同于下面的形式:

with mgr1:
    with mgr2:
        ...
        with mgrn:
            pass
           

但是with語句本身已經支援了多個下文管理器的使用,是以nested的意義不是很大。我們可以寫一個例子來看下nested函數的使用,以及與直接使用with來嵌套多個上下文管理器的差別,如下所示:

from contextlib import contextmanager
from contextlib import nested
from contextlib import closing

@contextmanager
def my_context(name):
    print("enter")
    try:
        yield name
    finally:
        print("exit")

#使用nested函數來調用多個管理器
print("---------使用nested函數調用多個管理器-----------")
with nested(my_context("管理器一"), my_context("管理器二"),my_context("管理器三")) as (m1,m2,m3):
    print(m1)
    print(m2)
    print(m3)

#直接使用with來調用調用多個管理器
print("---------使用with調用多個管理器-----------")
with my_context("管理器一") as m1, my_context("管理器二") as m2, my_context("管理器三") as m3:
    print(m1)
    print(m2)
    print(m3)
           
【Python學習筆記】with語句與上下文管理器with語句上下文管理器contextlib子產品

closing對象

contextlib中還包含一個closing對象,這個對象就是一個上下文管理器,它的__exit__函數僅僅調用傳入參數的close函數,closing對象的源碼如下:

class closing(object):
    def __init__(self, thing):
        self.thing = thing
    def __enter__(self):
        return self.thing
    def __exit__(self, *exc_info):
        self.thing.close()
           

是以closeing上下文管理器僅使用于具有close()方法的資源對象。例如,如果我們通過urllib.urlopen打開一個網頁,urlopen傳回的request有close方法,是以我們就可以使用closing上下文管理器,如下:

import urllib, sys
from contextlib import closing

with closing(urllib.urlopen('http://www.yahoo.com')) as f:
    for line in f:
        sys.stdout.write(line)
           
轉載自:【Python學習筆記】with語句與上下文管理器