上下文管理
文件IO操作可以对文件对象使用上下文管理,使用with……as语法。
with open("test") as f:
pass
仿照上例写一个自己的类,实现上下文管理。
class Point:
pass
with Point() as p:
pass
结果为:
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-1-77f0ac1b02c7> in <module>
2 pass
3
----> 4 with Point() as p:
5 pass
AttributeError: __enter__
提示属性错误,没有__exit__,看了需要这个属性。
上下文管理对象
当一个对象同时实现了__enter__()和__exit__()方法,它就属于上下文管理对象。
__enter__():进入于此相关的上下文。如果存在该方法,with语法会把该方法的返回值作为绑定到as子句中指定的变量上。
__exit__:退出与此对象相关的上下文。
class Point:
def __init__(self):
print("init")
def __enter__(self):#进去的时候做的事
print("enter")
def __exit__(self,exc_type,exc_val,exc_tb):#离开的时候做的事情
print("exit")
with Point() as f:
print("do sth.")
结果为:
init
enter
do sth.
exit
实例化对象的时候,并不会调用enter,进入with语句块调用__enter__方法,然后执行语句块,最后离开with语句块的时候,调用__exit__方法。
with可以开启一个上下文运行环境,在执行前做一些准备工作,执行后做一些收尾工作。
上下文管理的安全性
看看异常对上下文管理的影响。
class Point:
def __init__(self):
print("init")
def __enter__(self):
print("enter")
def __exit__(self,exc_type,exc_val,exc_tb):
print("exit")
with Point() as f:
raise Exception("error")
print("do sth.")
结果为:
init
enter
exit
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-4-28a514a324bc> in <module>
10
11 with Point() as f:
---> 12 raise Exception("er")
13 print("do sth.")
Exception: er
可以看到在enter和exit照样执行,上下文管理是安全的。
极端的例子
调用sys.exit(),它会退出当前解释器。
打开Python解释器,在里面敲入sys.exit(),窗口直接关闭了,也就是说碰到这一句,Python运行环境直接退出了。
import sys
class Point:
def __init__(self):
print("init")
def __enter__(self):
print("enter")
def __exit__(self,exc_type,exc_val,exc_tb):
print("exit")
with Point() as f:
sys.exit(-100)
print("do sth.")
print("outer")
结果为:
init
enter
exit
An exception has occurred, use %tb to see the full traceback.
SystemExit: -100
d:\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py:3304: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.
warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)
从执行结果来看,依然执行了__exit__函数,哪怕是退出Python运行环境,说明上下文管理很安全。
class Point:
def __init__(self):
print("init")
def __enter__(self):
print("enter"+self.__class__.__name__)
return self
def __exit__(self,exc_type,exc_val,exc_tb):
print("exit"+self.__class__.__name__)
print(exc_type)
print(exc_val)
print(exc_tb)
return 1
p = Point()
with p as f:
raise Exception("error123")
print(f==p)
print(f is p)
print(f)
print(p)
结果为:
init
enterPoint
exitPoint
<class 'Exception'>
error123
<traceback object at 0x03AE2FD0>
class Point:
def __init__(self):
print("init")
def __enter__(self):
print("enter"+self.__class__.__name__)
return self
def __exit__(self,exc_type,exc_val,exc_tb):
print("exit"+self.__class__.__name__)
print(exc_type)
print(exc_val)
print(exc_tb)
return 0
p = Point()
with p as f:
raise Exception("error123")
print(f==p)
print(f is p)
print(f)
print(p)
结果为:
init
enterPoint
exitPoint
<class 'Exception'>
error123
<traceback object at 0x03D30F58>
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-6-628d15869af5> in <module>
16 p = Point()
17 with p as f:
---> 18 raise Exception("error123")
19
20 print(f==p)
Exception: error123
由上面的例子可以看到,返回值很重要,一个返回值可以压制异常,是外面管还是里面管,返回等效true的话就是里面管,返回false的话就是外面管。所以这两个函数的返回值都很重要。
with语句
class Point:
def __init__(self):
print("init")
def __enter__(self):
print("enter")
def __exit__(self,exc_type,exc_val,exc_tb):print("exit")
p = Point()
with p as f:
print(p == f)#为什么不相等
print("do sth.")
结果为:
init enter False do sth. exit
问题在于__enter__方法,它将自己的返回值赋给f,修改上例。
class Point:
def __init__(self):
print("init")
def __enter__(self):
print("enter")
return self
def __exit__(self,exc_type,exc_val,exc_tb):
print("exit")
p = Point()
with p as f:
print(p == f)#为什么不相等
print("do sth.")
结果为:
init
enter
True
do sth.
exit
__enter__方法返回值就是上下文中使用的对象,with语法会把它的返回值赋给as子句的变量。
__enter__方法和__exit__方法的参数
__enter__方法没有其他参数。
__exit__方法有三个参数。
__exit__(self,exc_type,exc_value,traceback)
这三个参数都是与异常相关,如果该上下文退出时没有异常,这3个参数都为None。如果有异常,参数意义如下:
exc_type:异常类型
exc_vaule:异常的值
traceback:异常的追踪信息。
__exit__方法返回一个等效true的值,则压制异常,否则,继续抛出异常。
class Point:
def __init__(self):
print("init")
def __enter__(self):
print("enter")
def __exit__(self,exc_type,exc_val,exc_tb):
print(exc_type)
print(exc_val)
print(exc_tb)
print("exit")
return "abc"
p = Point()
with p as f:
raise Exception("new error")
print("do sth.")
print("outer")
结果为:
init
enter
<class 'Exception'>
new error
<traceback object at 0x0000000005A7A088>
exit
outer
练习
为加法函数计时
方法1、使用装饰器显示该函数的执行时长
方法2、商用上下文管理方法来显示该函数的执行时长。
import time
def add(x,y):
time.sleep(2)
return x+y
装饰器实现
import time
import datetime
from functools import wraps
def timeit(fn):
@wraps(fn)
def wrapper(*args,**kwargs):
start = datetime.datetime.now()
ret = fn(*args,**kwargs)
delta = (datetime.datetime.now()-start).total_seconds()
print("{} took {}s".format(fn.__name__,delta))
return ret
return wrapper
@timeit
def add(x,y):
time.sleep(2)
return x+y
print(add(4,5))
结果为:
add took 2.0s
9
上下文管理
import time
import datetime
from functools import wraps
def add(x,y):
time.sleep(2)
return x+y
class Timeit():
def __init__(self,fn):
self.fn = fn
def __enter__(self):
self.start =datetime.datetime.now()
def __exit__(self,exc_type,exc_val,exc_tb):
delta = (datetime.datetime.now()-self.start).total_seconds()
print("{} took {}s".format(self.fn.__name__,delta))
with Timeit(add) as fn:
print(add(4,7))
结果为:
11
add took 2.001s
另一种实现,使用可调用对象实现。
import time
import datetime
from functools import wraps
def add(x,y):
time.sleep(2)
return x+y
class Timeit():
def __init__(self,fn):
self.fn = fn
def __enter__(self):
self.start =datetime.datetime.now()
return self
def __exit__(self,exc_type,exc_val,exc_tb):
delta = (datetime.datetime.now()-self.start).total_seconds()
print("{} took {}s".format(self.fn.__name__,delta))
def __call__(self,x,y):
print(x,y)
return self.fn(x,y)
with Timeit(add) as timeitobj:
print(timeitobj(4,5))
根据上面的代码,能不能把类当做装饰器用?
思考,如何解决文档字符串问题?
方法一:直接修改__doc__
class Timeit:
def __init__(self,fn = None):
self.fn = fn
#把函数对象的文档字符串赋给类
self.__doc__ = fn.__doc__
方法二:使用functools.wraps函数
import time
import datetime
from functools import wraps
class Timeit:
""" this is a class"""
def __init__(self,fn):
self.fn = fn
#把函数对象的文档字符串赋值给类
self.__doc__ = fn.__doc__
#update_wrapper(self,fn)
def __enter__(self):
self.start = datetime.datetime.now()
return self
def __exit__(self,exc_type,exc_val,exc_tb):
delta = (datetime.datetime.now()-self.start).total_seconds()
print("{} took {}s".format(self.fn.__name__,delta))
def __call__(self,*args,**kwargs):
self.start = datetime.datetime.now()
ret = self.fn(*args,**kwargs)
delta = (datetime.datetime.now()-self.start).total_seconds()
print("{} took {}s".format(self.fn.__name__,delta))
return ret
@Timeit
def add(x,y):
""" this is add function"""
time.sleep(2)
return x+y
print(add(10,5))
print(add.__doc__)
print(Timeit(add).__doc__)
上面的类即可以用在上下文管理,又可以用作装饰器,初始化init函数不写return不代表返回是none,它返回的是实例。
上下文应用场景
1.增强功能
在代码执行的前后增加代码,以增强其功能,类似装饰器的功能。
2.资源管理
打开了资源需要关闭,例如文件对象,网络连接,数据库连接
3.权限验证
在执行代码之前,做权限的验证,在__enter__中处理。
contexlib.contextanmger
contexlib.contextanmger是一个装饰器实现上下文管理, 装饰一个函数,而不用像类一样实现__enter__和__exit__方法。
对下面的函数有要求,必须有yield,也就是这个函数必须返回一个生成器,且只有yield一个值,也就是这个装饰器接收一个生成器对象作为参数。
import contextlib
@contextlib.contextmanager
def foo():
print("enter")#相当于__enter__()
yield #yield 5,yield的值只能有一个,作为__enter__方法的返回值
print("exit") #相当于__exit__()
with foo() as f:
#raise Exception()
print(f)
结果为:
enter None exit
import contextlib
@contexlib.contextanmger
def foo():
print("enter")#相当于__enter__()
yield #yield 5,yield的值只能有一个,作为__enter__方法的返回值
print("exit") #相当于__exit__()
with foo() as f:
#raise Exception()
print(f)
结果为:
enter None exit
f接收yield语句的返回值
上面的程序看似不错,但是,增加一个异常试一试,发现不能保证exit的执行,怎么办?
import contextlib
@contextlib.contextmanager
def foo():
print("enter")#相当于__enter__()
try:
yield #yield 5,yield的值只能有一个,作为__enter__方法的返回值
finally:
print("exit")
with foo() as f:
raise Exception()
print(f)
结果为:
enter
exit
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-37-36a79d2a42b7> in <module>
10
11 with foo() as f:
---> 12 raise Exception()
13 print(f)
Exception:
上面这么做有什么意义呢?
当yield发生处为生成器函数增加了上下文管理,这是为函数增加上下文机制的方式。
- 把yield之前的当做__enter__方法执行
- 把yield之后的当做__exit__方法执行
- 把yield的值作为__enter__的返回值
import contextlib
import time
import datetime
@contextlib.contextmanager
def add(x,y):#为生成器函数增加了上下文管理
start= datetime.datetime.now()
try:
yield x+y#yield5,yield的值只能有一个,作为__enter__方法的返回值
finally:
delta = (datetime.datetime.now()-start).total_seconds()
with add(4,5) as f:
#raise Exception()
time.sleep(2)
print(f)
结果为:
9
总结:
如果业务逻辑简单可以使用函数加contextlib.contextmanager装饰器方式,如果业务复杂,用类的方式加__enter__和__exit__方法方便。