魔术方法2
上下文管理对象
当一个对象同时实现了__enter__()
和__exit__()
方法,它就属于上下文管理对象
import time
class Point:
def __init__(self):
print('init ~~~~~~~~~~~~~~~')
time.sleep(1)
print('init over')
def __enter__(self):
print('enter ~~~~~~~~~~~~~~')
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit ========================')
with Point() as p:
print('in with------------')
time.sleep(2)
print('with over')
print('=============end==============')
init ~~~~~~~~~~~~~~~
init over
enter ~~~~~~~~~~~~~~
in with------------
with over
exit ========================
=============end==============
实例化对象的时候,并不会调用enter,进入with语句块调用__enter__
方法,然后执行语句体,最后离开with语句块的时候,调用__exit__
方法。
with可以开启一个上下文运行环境,在执行前做一些准备工作,执行后做一些收尾工作。
注意:with并不开启一个新的作用域。
上下文管理的安全性
看看异常对上下文的影响
import time
class Point:
def __init__(self):
print('init ~~~~~~~~')
time.sleep(1)
print('init over')
def __enter__(self):
print('enter ~~~~~~~~')
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit =============')
with Point() as p:
print('in with ---------')
raise Exception('error')
time.sleep(2)
print('with over')
print('=============end===============')
init ~~~~~~~~
init over
enter ~~~~~~~~
in with ---------
exit =============
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-6-a9563eb44b9a> in <module>
15 with Point() as p:
16 print('in with ---------')
---> 17 raise Exception('error')
18 time.sleep(2)
19 print('with over')
Exception: error
可以看出enter和exit照样执行,上下文管理是安全的。
极端的例子
调用sys.exit(),它会退出当前解释器。
打开Python解释器,在里面敲入sys.exit(),窗口直接关闭了。也就是说碰到这一句,Python运行环境直接退出了。
import time
class Point:
def __init__(self):
print('init ~~~~~~~~')
time.sleep(1)
print('init over')
def __enter__(self):
print('enter ~~~~~~~~')
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit =============')
with Point() as p:
print('in with ---------')
import sys
sys.exit(1)
time.sleep(2)
print('with over')
print('=============end===============')
init ~~~~~~~~
init over
enter ~~~~~~~~
in with ---------
exit =============
An exception has occurred, use %tb to see the full traceback.
SystemExit: 1
d:\miniconda3\lib\site-packages\IPython\core\interactiveshell.py:3334: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.
warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)
从执行结果来看,依然执行了__exit__
函数,哪怕是退出Python运行环境。
说明上下文管理很安全。
with语句
# t3.py文件中写入下面代码
class Point:
def __init__(self):
print('init ~~~~~~~~')
def __enter__(self):
print('enter ~~~~~~~~')
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit =============')
f = open('t3.py')
with f as p:
print(f)
print(p)
print(f is p)
print(f == p)
# p = f = None
p = Point()
with p as f:
print('in with ------------')
print(p == f) # enter方法将自己的返回值赋给了f
print('with over')
print('=========end============')
<_io.TextIOWrapper name='t3.py' mode='r' encoding='cp936'>
<_io.TextIOWrapper name='t3.py' mode='r' encoding='cp936'>
True
True
init ~~~~~~~~
enter ~~~~~~~~
in with ------------
False
with over
exit =============
=========end============
问题在于__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('in with ------------')
print(p == f) # enter方法将自己的返回值赋给了f
print('with over')
print('=========end============')
init ~~~~~~~~
enter ~~~~~~~~
in with ------------
True
with over
exit =============
=========end============
with语法,会调用with后的对象的__enter__
方法,如果有as,则将该方法的返回值赋给as子句的变量。
上例,可以等价为f = p.__enter__()
方法的参数
__enter__
方法 没有其他参数
__exit__
方法有三个参数:
__exit__(self, exc_type, exc_value, exc_tb)
这三个参数都与异常有关,这三个参数都为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(1, exc_type)
print(2, exc_val)
print(3, exc_tb)
print('exit ============')
return 1
p = Point()
with p as f:
print('in with ------------')
raise Exception('Error')
print('with over')
print('=========end============')
init ~~~~~~~~
enter ~~~~~~~~
in with ------------
1 <class 'Exception'>
2 Error
3 <traceback object at 0x0000023295B70588>
exit ============
=========end============
练习
为加法函数计时
方法1、使用装饰器显示该函数的执行时长
方法2、使用上下文管理方法来显示该函数的执行时长
装饰器实现
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(delta)
return ret
return wrapper
@timeit
def add(x, y):
time.sleep(2)
return x + y
print(add(4, 5))
2.003499
9
上下文实现
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(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(delta)
with Timeit(add) as f:
print(add(4, 6))
2.000753
10
2.000753
将类当作装饰器用
import time
import datetime
from functools import wraps
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(delta)
def __call__(self, *args):
self.start = datetime.datetime.now()
ret = self.fn(*args)
self.delta = (datetime.datetime.now() - self.start).total_seconds()
print(self.delta)
return ret
@Timeit
def add(x, y):
time.sleep(2)
return x + y
add(4, 5)
print(add.__doc__)
# print(add.__name__) # 异常,没有此属性
2.00617
None
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-24-afc72ebf40ba> in <module>
32 add(4, 5)
33 print(add.__doc__)
---> 34 print(add.__name__) # 异常,没有此属性
AttributeError: 'Timeit' object has no attribute '__name__'
思考
如何解决文档字符串问题?
方法一:直接修改__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)
wraps(fn)(self)
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(delta)
def __call__(self, *args):
self.start = datetime.datetime.now()
ret = self.fn(*args)
delta = (datetime.datetime.now() - self.start).total_seconds()
print(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__)
2.000463
15
this is add function
this is add function
上面的类既可以用在上下文管理,又可以用作装饰器
上下文应用场景
1、增强功能
- 在代码执行的前后增加代码,以增强其功能。类似装饰器的功能
2、资源管理
- 打开了资源需要关闭,例如文件对象,网络连接、数据库连接等
3、权限验证
- 在执行代码之前,做权限的验证,在
__enter__
中处理
contextlib.contextmanager
contextlib.contextmanager
它是一个装饰器实现上下文管理,装饰一个函数,而不用像类一样实现__enter__
和__exit__
方法。
对下面的函数有要求:必须有yield,也就是这个函数必须返回一个生成器,且只有yield一个值。
也就是这个装饰器接受一个生成器对象作为参数
当yield发生处生成器函数增加了上下文管理。这就是为函数增加上下文机制的方式。
- 把yield之前的当作
__enter__
方法执行 - 把yield之后的当作
__exit__
方法执行 - 把yield的值作为
__enter__
的返回值
import contextlib
import datetime
import time
@contextlib.contextmanager
def add(x, y): # 为生成器函数增加了上下文管理
start = datetime.datetime.now()
try:
time.sleep(2)
yield x + y # yield 5,yield的值只能有一个,作为`__enter__`方法的返回值
finally:
delta = (datetime.datetime.now() - start).total_seconds()
print(delta)
with add(4, 5) as f:
# raise Exception()
print(f)
2.000723
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
<ipython-input-28-fdd9d911ac66> in <module>
14
15 with add(4, 5) as f:
---> 16 raise Exception()
17 print(f)
Exception:
总结:
如果业务逻辑简单可以使用函数加contextlib.contextmanager装饰器方式,如果业务复杂,用类的方式加__enter__
和__exit__
方法方便。