python中 with 用法及原理(上下文管理器)
前言
with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭/线程中锁的自动获取和释放等。
问题引出
如下代码:
file = open("1.txt")data = file.read()
file.close()
上面代码存在2个问题:
①文件读取发生异常,但没有进行任何处理;
②可能忘记关闭文件句柄;
改进
try:f = open('xxx')
except:
print('fail to open')
exit(-1)
try:
do something
except:
do something
finally:
f.close()
虽然这段代码运行良好,但比较冗长。
而使用 with 语句的话,能够减少冗长,还能自动处理上下文环境产生的异常。如下面代码:
with open("1.txt") as file:data = file.read()
with 工作原理
①紧跟with后面的语句被求值后,返回对象的 __enter__ 方法被调用,返回值将被赋值给 as 后面的变量;
②当 with 语句体全部被执行完之后,将调用前面返回对象的 __exit__ 方法。
with工作原理代码示例:
class Sample:def __enter__(self):
print("in __enter__")
return "Foo"
def __exit__(self, exc_type, exc_val, exc_tb):
print("in __exit__")
def get_sample():
return Sample()
with get_sample() as sample:
print("Sample: ", sample)
运行结果:
整个运行过程如下:
(1) __enter__ 方法被执行;
(2) __enter__ 方法的返回值,在这个例子中是“Foo”,赋值给变量sample;
(3)执行代码块,打印sample变量的值为“Foo”;
(4) __exit__ 方法被调用;
【注意】 __exit__ 方法中有3个参数, exc_type , exc_val ,exc_tb ,这些参数在异常处理中相当有用。
参数解释:
exc_type :错误的类型
exc_val :错误类型对应的值
exc_tb :代码中错误发生的位置
示例代码:
class Sample:def __enter__(self):
print('in enter')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("type: ", exc_type)
print("val: ", exc_val)
print("tb: ", exc_tb)
def do_something(self):
bar = 1 / 0
return bar + 10
with Sample() as sample:
sample.do_something()
运行结果:
总结
实际上,在 with 后面的代码块抛出异常时, __exit__ 方法被执行。开发库时,清理资源,关闭文件等操作,都可以放在 __exit__ 方法中。
总之, with-as 表达式极大的简化了每次写 finally 的工作,这对代码的优雅性是有极大帮助的。
只要实现了 __enter__() 和 __exit__() 这两个方法的类都可以轻松创建上下文管理器,就能使用 with 。
如果有多项,可以这样写:
With open('1.txt') as f1, open('2.txt') as f2:do something
with语句的原理
- 上下文管理协议(Context Management Protocol):包含方法 __enter__() 和 __exit__() ,支持该协议的对象要实现这两个方法。
- 上下文管理器(Context Manager):支持上下文管理协议的对象,这种对象实现了 __enter__() 和 __exit__() 方法。上下文管理器定义执行with语句时要建立的运行时上下文,负责执行with语句块上下文中的进入与退出操作。通常使用with语句调用上下文管理器,也可以通过直接调用其方法来使用。
with语句的常用表达式:
with EXPR as VAR: # EXPR可以是任意表达式BLOCK
其一般的执行过程是这样的:
1、执行EXPR,生成上下文管理器context_manager;
2、获取上下文管理器的 __exit()__ 方法,并保存起来用于之后的调用;
3、调用上下文管理器的 __enter__() 方法;如果使用了as子句,则将 __enter__() 方法的返回值赋值给as子句中的VAR;
4、执行BLOCK中的表达式;
5、不管是否执行过程中是否发生了异常,执行上下文管理器的 __exit__() 方法, __exit__() 方法负责执行“清理”工作,如释放资源等。
如果执行过程中没有出现异常,或者with语句体中执行了语句 break/continue/return ,则以None作为参数调用 __exit__(None, None, None) ;如果执行过程中出现异常,则使用 sys.exc_info 得到的异常信息为参数调用 __exit__(exc_type, exc_value, exc_traceback) ;
6、出现异常时,如果 __exit__(type, value, traceback) 返回False,则会重新抛出异常,让with之外的语句逻辑来处理异常,这也是通用做法;如果返回True,则忽略异常,不再对异常进行处理。
自定义上下文管理器
python的 with 语句是提供一个有效的机制,让代码更简练,同时在异常产生时,清理工作更简单。
示例1:
class DBManager(object):def __init__(self):
pass
def __enter__(self):
print('__enter__')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('__exit__')
return True
def getInstance():
return DBManager()
with getInstance() as dbManagerIns:
print('with demo')
【注意】with后面必须跟一个上下文管理器,如果使用了as,则是把上下文管理器的 __enter__ 方法的返回值赋值给 target,target 可以是单个变量,或者由“()”括起来的元组【不能是仅仅由“,”分隔的变量列表,必须加“()”】
运行结果:
结果分析:当我们使用with语句的时候, __enter__ 方法被调用,并且将 __enter__ 方法返回值赋值给as后面的变量,并且在退出with的时候自动执行 __exit__ 方法。
示例2:
class With_work(object):def __enter__(self):
"""进入with语句的时候被调用"""
print('①enter called')
return "②打印对象f的值"
def __exit__(self, exc_type, exc_val, exc_tb):
"""离开with的时候被with调用"""
print('④exit called')
with With_work() as f:
print(f)
print('③打印with代码块中的输出')
print('⑤with代码块执行完毕之后的打印')
运行结果:
【注意】没有实现 __enter__() 和 __exit__() 这两个方法的类都不能创建上下文管理器,不能使用 with 语句。
例如:
class Door(object):def open(self):
print('Door is opened')
def close(self):
print('Door is closed')
with Door() as d:
d.open()
运行结果:
python中 from contextlib import closing 的使用
官方:https://docs.python.org/dev/library/contextlib.html
1、python中有些类没有实现 __enter__() 和 __exit__() 这两个方法也是可以使用 with 语句。但是前提是实现了 close() 语句。
例如:
import contextlibclass Door(object):
def open(self):
print('Door is opened')
def close(self):
print('Door is closed')
with contextlib.closing(Door()) as door:
door.open()
运行结果:
2、 contextlib.closing(xxx) 原理如下:
class closing(object):"""Context to automatically close something at the end of a block.
Code like this:
with closing(<module>.open(<arguments>)) as f:
<block>
is equivalent to this:
f = <module>.open(<arguments>)
try:
<block>
finally:
f.close()
"""
def __init__(self, thing):
self.thing = thing
def __enter__(self):
return self.thing
def __exit__(self, *exc_info):
self.thing.close()
contextlib.closing() 会自动帮某些类加上 __enter__() 和 __exit__() 这两个方法,使其满足上下文管理器的条件。
3、并不是只有类才能满足上下文管理器的条件,其实方法也可以实现一个上下文管理器【contextlib.contextmanager】
可以通过 @contextlib.contextmanager 装饰器的方式实现,但是其装饰的方法必须是一个生成器。 yield 关键字前半段用来表示 __enter__() 方法, yield 关键字后半段用来表示 __exit__() 方法。
例如:
import contextlib@contextlib.contextmanager
def tag(name):
print("<%s>" % name)
yield
print("</%s>" % name)
with tag(name="h1"):
print('hello world!')
运行结果:
4、使用 contextlib.contextmanager 实现装饰器才能做的事情
例如:比如给一段代码加时间花费计算。
普通装饰器版本:【此方法解决此类需求要更加方便美观】
import timedef wrapper(func):
def new_func(*args, **kwargs):
t1 = time.time()
ret = func(*args, **kwargs)
t2 = time.time()
print('cost time=', (t2 - t1))
return ret
return new_func
@wrapper
def hello(a, b):
time.sleep(1)
print('a + b = ', a + b)
if __name__ == '__main__':
hello(100, 200)
运行结果:
contextlib.contextmanger版本:
import timeimport contextlib
@contextlib.contextmanager
def cost_time():
t1 = time.time()
yield
t2 = time.time()
print('cost time=', t2 - t1)
with cost_time():
time.sleep(1)
a = 100
b = 200
print('a + b = ', a + b)
原理:
1、因为cost_time()方法是个生成器,所以运行__enter__()的时候,contextmanager调用 self.gen.next() 会跑到cost_time()方法的yield处,停住挂起,这个时候已经有了t1=time.time();
2、然后运行with语句体里面的语句,也就是a+b=300;
3、跑完后运行 __exit__() 的时候,contextmanager调用 self.gen.next() 会从cost_time()的yield的下一句开始一直到结束。这个时候有了t2=time.time(),t2-t1从而实现了统计cost_time的效果。
5、 contextlib.contextmanager 源码如下:
import sysclass GeneratorContextManager(object):
"""Helper for @contextmanager decorator."""
def __init__(self, gen):
self.gen = gen
def __enter__(self):
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("generator didn't yield")
def __exit__(self, type, value, traceback):
if type is None:
try:
self.gen.next()
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
if value is None:
# Need to force instantiation so we can reliably
# tell if we get the same exception back
value = type()
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration as exc:
return exc is not value
except:
if sys.exc_info()[1] is not value:
raise
def contextmanager(func):
@wraps(func)
def helper(*args, **kwds):
return GeneratorContextManager(func(*args, **kwds))
return helper
去期待陌生,去拥抱惊喜。