with 语句和上下文管理器
- # create/aquire some resource
- ...
- try:
- # do something with the resource
- ...
- finally:
- # destroy/release the resource
- ...
处理文件,线程,数据库,网络编程等等资源的时候,我们经常需要使用上面这样的代码形式,以确保资源的正常使用和释放。
好在Python
提供了 with
语句帮我们自动进行这样的处理,例如之前在打开文件时我们使用:
In [1]:
- with open('my_file', 'w') as fp:
- # do stuff with fp
- data = fp.write("Hello world")
这等效于下面的代码,但是要更简便:
In [2]:
- fp = open('my_file', 'w')
- try:
- # do stuff with f
- data = fp.write("Hello world")
- finally:
- fp.close()
上下文管理器
其基本用法如下:
with <expression>:
<block>
<expression>
执行的结果应当返回一个实现了上下文管理器的对象,即实现这样两个方法,enter
和 exit
:
In [3]:
- print fp.__enter__
- print fp.__exit__
- <built-in method __enter__ of file object at 0x0000000003C1D540>
- <built-in method __exit__ of file object at 0x0000000003C1D540>
enter
方法在 <block>
执行前执行,而 exit
在 <block>
执行结束后执行:
比如可以这样定义一个简单的上下文管理器:
In [4]:
- class ContextManager(object):
- def __enter__(self):
- print "Entering"
- def __exit__(self, exc_type, exc_value, traceback):
- print "Exiting"
使用 with
语句执行:
In [5]:
- with ContextManager():
- print " Inside the with statement"
- Entering
- Inside the with statement
- Exiting
即使 <block>
中执行的内容出错,exit
也会被执行:
In [6]:
- with ContextManager():
- print 1/0
- Entering
- Exiting
- ---------------------------------------------------------------------------
- ZeroDivisionError Traceback (most recent call last)
- <ipython-input-6-b509c97cb388> in <module>()
- 1 with ContextManager():
- ----> 2 print 1/0
- ZeroDivisionError: integer division or modulo by zero
enter 的返回值
如果在 enter
方法下添加了返回值,那么我们可以使用 as
把这个返回值传给某个参数:
In [7]:
- class ContextManager(object):
- def __enter__(self):
- print "Entering"
- return "my value"
- def __exit__(self, exc_type, exc_value, traceback):
- print "Exiting"
将 enter
返回的值传给 value
变量:
In [8]:
- with ContextManager() as value:
- print value
- Entering
- my value
- Exiting
一个通常的做法是将 enter
的返回值设为这个上下文管理器对象本身,文件对象就是这样做的:
In [9]:
- fp = open('my_file', 'r')
- print fp.__enter__()
- fp.close()
- <open file 'my_file', mode 'r' at 0x0000000003B63030>
In [10]:
- import os
- os.remove('my_file')
实现方法非常简单:
In [11]:
- class ContextManager(object):
- def __enter__(self):
- print "Entering"
- return self
- def __exit__(self, exc_type, exc_value, traceback):
- print "Exiting"
In [12]:
- with ContextManager() as value:
- print value
- Entering
- <__main__.ContextManager object at 0x0000000003D48828>
- Exiting
错误处理
上下文管理器对象将错误处理交给 exit
进行,可以将错误类型,错误值和 traceback
等内容作为参数传递给 exit
函数:
In [13]:
- class ContextManager(object):
- def __enter__(self):
- print "Entering"
- def __exit__(self, exc_type, exc_value, traceback):
- print "Exiting"
- if exc_type is not None:
- print " Exception:", exc_value
如果没有错误,这些值都将是 None
, 当有错误发生的时候:
In [14]:
- with ContextManager():
- print 1/0
- Entering
- Exiting
- Exception: integer division or modulo by zero
- ---------------------------------------------------------------------------
- ZeroDivisionError Traceback (most recent call last)
- <ipython-input-14-b509c97cb388> in <module>()
- 1 with ContextManager():
- ----> 2 print 1/0
- ZeroDivisionError: integer division or modulo by zero
在这个例子中,我们只是简单的显示了错误的值,并没有对错误进行处理,所以错误被向上抛出了,如果不想让错误抛出,只需要将 exit
的返回值设为 True
:
In [15]:
- class ContextManager(object):
- def __enter__(self):
- print "Entering"
- def __exit__(self, exc_type, exc_value, traceback):
- print "Exiting"
- if exc_type is not None:
- print " Exception suppresed:", exc_value
- return True
In [16]:
- with ContextManager():
- print 1/0
- Entering
- Exiting
- Exception suppresed: integer division or modulo by zero
在这种情况下,错误就不会被向上抛出。
数据库的例子
对于数据库的 transaction 来说,如果没有错误,我们就将其 commit
进行保存,如果有错误,那么我们将其回滚到上一次成功的状态。
In [17]:
- class Transaction(object):
- def __init__(self, connection):
- self.connection = connection
- def __enter__(self):
- return self.connection.cursor()
- def __exit__(self, exc_type, exc_value, traceback):
- if exc_value is None:
- # transaction was OK, so commit
- self.connection.commit()
- else:
- # transaction had a problem, so rollback
- self.connection.rollback()
建立一个数据库,保存一个地址表:
In [18]:
- import sqlite3 as db
- connection = db.connect(":memory:")
- with Transaction(connection) as cursor:
- cursor.execute("""CREATE TABLE IF NOT EXISTS addresses (
- address_id INTEGER PRIMARY KEY,
- street_address TEXT,
- city TEXT,
- state TEXT,
- country TEXT,
- postal_code TEXT
- )""")
插入数据:
In [19]:
- with Transaction(connection) as cursor:
- cursor.executemany("""INSERT OR REPLACE INTO addresses VALUES (?, ?, ?, ?, ?, ?)""", [
- (0, '515 Congress Ave', 'Austin', 'Texas', 'USA', '78701'),
- (1, '245 Park Avenue', 'New York', 'New York', 'USA', '10167'),
- (2, '21 J.J. Thompson Ave.', 'Cambridge', None, 'UK', 'CB3 0FA'),
- (3, 'Supreme Business Park', 'Hiranandani Gardens, Powai, Mumbai', 'Maharashtra', 'India', '400076'),
- ])
假设插入数据之后出现了问题:
In [20]:
- with Transaction(connection) as cursor:
- cursor.execute("""INSERT OR REPLACE INTO addresses VALUES (?, ?, ?, ?, ?, ?)""",
- (4, '2100 Pennsylvania Ave', 'Washington', 'DC', 'USA', '78701'),
- )
- raise Exception("out of addresses")
- ---------------------------------------------------------------------------
- Exception Traceback (most recent call last)
- <ipython-input-20-ed8abdd56558> in <module>()
- 3 (4, '2100 Pennsylvania Ave', 'Washington', 'DC', 'USA', '78701'),
- 4 )
- ----> 5 raise Exception("out of addresses")
- Exception: out of addresses
那么最新的一次插入将不会被保存,而是返回上一次 commit
成功的状态:
In [21]:
- cursor.execute("SELECT * FROM addresses")
- for row in cursor:
- print row
- (0, u'515 Congress Ave', u'Austin', u'Texas', u'USA', u'78701')
- (1, u'245 Park Avenue', u'New York', u'New York', u'USA', u'10167')
- (2, u'21 J.J. Thompson Ave.', u'Cambridge', None, u'UK', u'CB3 0FA')
- (3, u'Supreme Business Park', u'Hiranandani Gardens, Powai, Mumbai', u'Maharashtra', u'India', u'400076')
contextlib 模块
很多的上下文管理器有很多相似的地方,为了防止写入很多重复的模式,可以使用 contextlib
模块来进行处理。
最简单的处理方式是使用 closing
函数确保对象的 close()
方法始终被调用:
In [23]:
- from contextlib import closing
- import urllib
- with closing(urllib.urlopen('http://www.baidu.com')) as url:
- html = url.read()
- print html[:100]
- <!DOCTYPE html><!--STATUS OK--><html><head><meta http-equiv="content-type" content="text/html;charse
另一个有用的方法是使用修饰符 @contextlib
:
In [24]:
- from contextlib import contextmanager
- @contextmanager
- def my_contextmanager():
- print "Enter"
- yield
- print "Exit"
- with my_contextmanager():
- print " Inside the with statement"
- Enter
- Inside the with statement
- Exit
yield
之前的部分可以看成是 enter
的部分,yield
的值可以看成是 enter
返回的值,yield
之后的部分可以看成是 exit
的部分。
使用 yield
的值:
In [25]:
- @contextmanager
- def my_contextmanager():
- print "Enter"
- yield "my value"
- print "Exit"
- with my_contextmanager() as value:
- print value
- Enter
- my value
- Exit
错误处理可以用 try
块来完成:
In [26]:
- @contextmanager
- def my_contextmanager():
- print "Enter"
- try:
- yield
- except Exception as exc:
- print " Error:", exc
- finally:
- print "Exit"
In [27]:
- with my_contextmanager():
- print 1/0
- Enter
- Error: integer division or modulo by zero
- Exit
对于之前的数据库 transaction
我们可以这样定义:
In [28]:
- @contextmanager
- def transaction(connection):
- cursor = connection.cursor()
- try:
- yield cursor
- except:
- connection.rollback()
- raise
- else:
- connection.commit()