上下文管理
文件IO操作可以对文件对象使用上下文管理,使用with … as 语法。
with open('test') as f:
pass
仿照上例写一个自己的类,实现上下文管理
class Point:
pass
with Point() as f: # AttributeError: __exit__
pass
提示属性错误, 没有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
raise Exception('error')
可以看出在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
Process finished with exit code -100
从执行结果来看,依然执行了exit函数,哪怕是退出Python运行环境。
说明上下文管理很安全
。
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 Point() as f:
print(p == f) # 为什么不相等
print('do sth.')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
init
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方法和exit方法的参数
enter方法,没有其他参数。
exit方法有3个参数:
exit(self, exc_type, exc_val, exc_tb):
这三个参数都与异常有关。
如果该上下文退出时没有异常,这3个参数都为None。
如果有异常,参数意义如下:
参数 | 意义 |
---|---|
exc_type | 异常类型 |
exc_value | 异常的值 |
exc_tb | 异常的追踪信息 |
exit方法返回一个等效True的值,则压制异常;否则抛出异常
class Point:
def __init__(self):
print('init')
def __enter__(self):
print('enter')
return self
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.')
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
init
enter
<class 'Exception'>
New Error
<traceback object at 0x00000145D258F808>
exit
练习
- 为加法函数计时
- 方法1:使用装饰器显示该函数的执行时长
- 方法2:使用上下文管理方法来显示该函数的执行时长
import time
def add(x, y):
time.sleep(2)
return x + y
装饰器实现1
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.000429s
9
装饰器实现2
import time
import datetime
def timeit(output=lambda fn, delta: print("{} took {}s".format(fn.__name__, delta))):
def timeit(fn):
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
ret = fn(*args, **kwargs)
delta = (datetime.datetime.now() - start).total_seconds()
output(fn, delta)
return ret
return wrapper
return timeit
@timeit() # add = timeit(add)
def add(x, y):
time.sleep(2)
return x + y
add(4, 5) # wrapper
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.016718s
上下文管理
基本实现
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
class TimeIt:
def __init__(self, fn):
self.fn = fn
def __enter__(self):
self.start = datetime.datetime.now()
return self.fn
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(fn(4, 6))
print(add(4, 5))
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.000976s
10
add took 2.000243s
9
add took 4.001219s
另一种实现,使用可调用对象实现
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
class TimeIt:
def __init__(self, fn):
self.fn = fn
def __enter__(self):
self.start = datetime.datetime.now()
return self.fn
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, 6))
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.000261s
10
add took 2.000261s
根据上面的代码,能不能把类当做装饰器用?
import time
import datetime
from functools import wraps
class TimeIt:
"""This is TimeIt function."""
def __init__(self, fn):
self.fn = fn
def __enter__(self):
self.start = datetime.datetime.now()
return self.fn
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)
self.delta = (datetime.datetime.now() - self.start).total_seconds()
print("{} took {}s. call".format(self.fn.__name__, self.delta))
return ret
@TimeIt
def add(x, y):
"""This is add function."""
time.sleep(2)
return x + y
add(4, 5)
print(add.__doc__)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.000246s. call
This is TimeIt function.
思考:如何解决文档字符串问题?
方法一:直接修改doc
class TimeIt:
"""This is TimeIt function."""
def __init__(self, fn):
self.fn = fn
# 把函数对象的文档字符串赋给类
self.__doc__ = fn.__doc__
方法二:使用functools.wraps函数
import time
import datetime
from functools import wraps, update_wrapper
class TimeIt:
"""This is TimeIt function."""
def __init__(self, fn):
self.fn = fn
# 把函数对象的文档字符串赋给类
# self.__doc__ = fn.__doc__
# update_wrapper(self, fn)
wraps(fn)(self)
def __enter__(self):
self.start = datetime.datetime.now()
return self.fn
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)
self.delta = (datetime.datetime.now() - self.start).total_seconds()
print("{} took {}s. call".format(self.fn.__name__, self.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__)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
add took 2.000424s. call
15
This is add function.
This is add function.
上面的类既可以用在上下文管理,有可以用作装饰器
上下文应用场景
- 增强功能
- 在代码执行的前后增加代码,以增强其功能。类似装饰器的功能。
- 资源管理
- 打开了资源需要关闭,例如文件对象、网络连接、数据库连接等
- 权限验证
- 在执行代码之前,做权限的验证,在enter中处理
contextlib.contextmanger
contextlib.contextmanger
它是一个装饰器实现上下文管理,装饰一个函数,而不用像类一样实现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
f接收yield语句的返回值。
上面的额程序看似不错,但是,增加一个异常试试,发现不能保证exit的执行,怎么办?增加try finally
import contextlib
@contextlib.contextmanager
def foo():
print('enter') # 相当于__enter__()
try:
yield # yield 5, yield的值只能有一个,作为__enter__方法的返回值
finally:
print('exit') # 相当于__exit__()
with foo() as f:
raise Exception()
print(f)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
enter
None
exit
Exception
上例这么做有什么意义呢?
- 当yield发生处为生成器函数增加了上下文管理。这是为函数增加上下文机制的方法。
- 把yield之前的当做enter方法执行
- 把yield之后的当做exit方法执行
- 把yield的值作为enter的返回值
import contextlib
import datetime
import time
@contextlib.contextmanager
def add(x, y): # 为生成器函数增加了上下文管理
start = datetime.datetime.now()
try:
yield x + y# yield 5, yield的值只能有一个,作为__enter__方法的返回值
finally:
delta = (datetime.datetime.now() - start).total_seconds()
print("{}s".format(delta))
with add(4, 5) as f:
# raise Exception()
time.sleep(2)
print(f)
~~~~~~~~~~~~~~~~~~~~
9
2.000861s
contextlib.contextmangager总结
如果业务逻辑简单可以使用函数加contextlib.contextmanager装饰器方法,如果业务复杂,用类的方式加enter和exit方法方便。
# 使用装饰器显示该函数的执行时长
import time
import datetime
def logger(fn):
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
x = fn(*args, **kwargs)
end = datetime.datetime.now()
interval = (end - start).total_seconds()
print("function time = {}".format(interval))
return x
return wrapper
@logger
def add(x, y):
time.sleep(2)
return x + y
print(add(3, 4))
# 使用装饰器显示该函数的执行时长
import time
import datetime
class Add:
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
args, kwargs = self.args, self.kwargs
def logger(fn):
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
x = fn(*args, **kwargs)
end = datetime.datetime.now()
interval = (end - start).total_seconds()
print("function time = {}".format(interval))
return x
return wrapper
@logger
def add(x, y):
time.sleep(2)
return x + y
addtest = Add()
print(addtest.add(3, 4))
# 使用装饰器显示该函数的执行时长
import time
import datetime
class Add:
def __init__(self, x, y):
self.x = x
self.y = y
def __enter__(self):
start = datetime.datetime.now()
return start
def __exit__(self, exc_type, exc_val, exc_tb):
end = datetime.datetime.now()
interval = (end - start).total_seconds()
return interval
addtest = Add()
def add(x, y):
time.sleep(2)
return x + y