详解利用上下文管理器扩展Python计时器
作者:云朵君 发布时间:2023-11-07 09:33:48
上文中,我们一起学习了手把手教你实现一个 Python 计时器。本文中,云朵君将和大家一起了解什么是上下文管理器 和 Python 的 with 语句,以及如何完成自定义。然后扩展 Timer
以便它也可以用作上下文管理器。最后,使用 Timer 作为上下文管理器如何简化我们自己的代码。
上文中我们创建的第一个 Python 计时器类,然后逐步扩展我们 Timer
类,其代码也是较为丰富强大。我们不能满足于此,仍然需要模板一些代码来使用Timer
:
首先,实例化类
其次,在要计时的代码块之前调用
.start()
最后,在代码块之后调用
.stop()
一个 Python 定时器上下文管理器
Python 有一个独特的构造,用于在代码块之前和之后调用函数:上下文管理器。
了解 Python 中的上下文管理器
上下文管理器长期以来一直是 Python 中重要的一部分。由 PEP 343 于 2005 年引入,并首次在 Python 2.5 中实现。可以使用 with
关键字识别代码中的上下文管理器:
with?EXPRESSION?as?VARIABLE:
????BLOCK
EXPRESSION
是一些返回上下文管理器的 Python 表达式。首先上下文管理器绑定到变量名 VARIABLE
上,BLOCK
可以是任何常规的 Python 代码块。上下文管理器保证程序在 BLOCK
之前调用一些代码,在 BLOCK
执行之后调用一些其他代码。这样即使 BLOCK
引发异常,后者也是会照样执行。
上下文管理器最常见的用途是处理不同的资源,如文件、锁和数据库连接等。上下文管理器用于使用资源后释放和清理资源。以下示例仅通过打印包含冒号的行来演示 timer.py
的基本结构。此外,它展示了在 Python 中打开文件的常用习语:
with?open("timer.py")?as?fp:
????print("".join(ln?for?ln?in?fp?if?":"?in?ln))
class?TimerError(Exception):
class?Timer:
????timers:?ClassVar[Dict[str,?float]]?=?{}
????name:?Optional[str]?=?None
????text:?str?=?"Elapsed?time:?{:0.4f}?seconds"
????logger:?Optional[Callable[[str],?None]]?=?print
????_start_time:?Optional[float]?=?field(default=None,?init=False,?repr=False)
????def?__post_init__(self)?->?None:
????????if?self.name?is?not?None:
????def?start(self)?->?None:
????????if?self._start_time?is?not?None:
????def?stop(self)?->?float:
????????if?self._start_time?is?None:
????????if?self.logger:
????????if?self.name:
注意,使用 open()
作为上下文管理器,文件指针fp
不会显式关闭,可以确认 fp
已自动关闭:
fp.closed
True
在此示例中,open("timer.py")
是一个返回上下文管理器的表达式。该上下文管理器绑定到名称 fp
。上下文管理器在 print()
执行期间有效。这个单行代码块在 fp
的上下文中执行。
fp
是上下文管理器是什么意思? 从技术上讲,就是 fp
实现了 上下文管理器协议。Python 语言底层有许多不同的协议。可以将协议视为说明我们代码必须实现哪些特定方法的合同。
上下文管理器协议由两种方法组成:
进入与上下文管理器相关的上下文时调用
.__enter__()
。退出与上下文管理器相关的上下文时调用
.__exit__()
。
换句话说,要自己创建上下文管理器,需要编写一个实现 .__enter__()
和 .__exit__()
的类。试试 Hello, World!
上下文管理器示例:
#?studio.py
class?Studio:
????def?__init__(self,?name):
????????self.name?=?name
????def?__enter__(self):
????????print(f"你好?{self.name}")
????????return?self
????def?__exit__(self,?exc_type,?exc_value,?exc_tb):
????????print(f"一会儿见,?{self.name}")
Studio
是一个上下文管理器,它实现了上下文管理器协议,使用如下:
from?studio?import?Studio
with?Studio("云朵君"):
????print("正在忙?...")
你好 云朵君
正在忙 ...
一会儿见, 云朵君
首先,注意 .__enter__()
在做事之前是如何被调用的,而 .__exit__()
是在做事之后被调用的。该示例中,没有引用上下文管理器,因此不需要使用 as
为上下文管理器命名。
接下来,注意 self.__enter__()
的返回值受 as
约束。创建上下文管理器时,通常希望从 .__enter__()
返回 self
。可以按如下方式使用该返回值:
from?greeter?import?Greeter
with?Greeter("云朵君")?as?grt:
??print(f"{grt.name}?正在忙?...")
你好 云朵君
云朵君 正在忙 ...
一会儿见, 云朵君
在写 __exit__
函数时,需要注意的事,它必须要有这三个参数:
exc_type
:异常类型exc_val
:异常值exc_tb
:异常的错误栈信息
这三个参数用于上下文管理器中的错误处理,它们以 sys.exc_info()
的返回值返回。当主逻辑代码没有报异常时,这三个参数将都为None。
如果在执行块时发生异常,那么代码将使用异常类型、异常实例和回溯对象(即exc_type
、exc_value
和exc_tb
)调用 .__exit__()
。通常情况下,这些在上下文管理器中会被忽略,而在引发异常之前调用 .__exit__()
:
from?greeter?import?Greeter
with?Greeter("云朵君")?as?grt:
????print(f"{grt.age}?does?not?exist")
你好 云朵君
一会儿见, 云朵君
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AttributeError: 'Greeter' object has no attribute 'age'
可以看到,即使代码中有错误,还是照样打印了 "一会儿见, 云朵君"
。
理解并使用 contextlib
现在我们初步了解了上下文管理器是什么以及如何创建自己的上下文管理器。在上面的例子中,我们只是为了构建一个上下文管理器,却写了一个类。如果只是要实现一个简单的功能,写一个类未免有点过于繁杂。这时候,我们就想,如果只写一个函数就可以实现上下文管理器就好了。
这个点Python早就想到了。它给我们提供了一个装饰器,你只要按照它的代码协议来实现函数内容,就可以将这个函数对象变成一个上下文管理器。
我们按照 contextlib 的协议来自己实现一个上下文管理器,为了更加直观我们换个用例,创建一个我们常用且熟悉的打开文件(with open)的上下文管理器。
import?contextlib
@contextlib.contextmanager
def?open_func(file_name):
????#?__enter__方法
????print('open?file:',?file_name,?'in?__enter__')
????file_handler?=?open(file_name,?'r')
?
????#?【重点】:yield
????yield?file_handler
????#?__exit__方法
????print('close?file:',?file_name,?'in?__exit__')
????file_handler.close()
????return
with?open_func('test.txt')?as?file_in:
????for?line?in?file_in:
????????print(line)
在被装饰函数里,必须是一个生成器(带有yield
),而 yield
之前的代码,就相当于__enter__
里的内容。yield
之后的代码,就相当于__exit__
里的内容。
上面这段代码只能实现上下文管理器的第一个目的(管理资源),并不能实现第二个目的(处理异常)。
如果要处理异常,可以改成下面这个样子。
import?contextlib
@contextlib.contextmanager
def?open_func(file_name):
????#?__enter__方法
????print('open?file:',?file_name,?'in?__enter__')
????file_handler?=?open(file_name,?'r')
????try:
????????yield?file_handler
????except?Exception?as?exc:
????????#?deal?with?exception
????????print('the?exception?was?thrown')
????finally:
????????print('close?file:',?file_name,?'in?__exit__')
????????file_handler.close()
????????return
with?open_func('test.txt')?as?file_in:
????for?line?in?file_in:
????????1/0
????????print(line)
Python 标准库中的 contextlib
包括定义新上下文管理器的便捷方法,以及可用于关闭对象、抑制错误甚至什么都不做的现成上下文管理器!
创建 Python 计时器上下文管理器
了解了上下文管理器的一般工作方式后,要想知道它们是如何帮助处理时序代码呢?假设如果可以在代码块之前和之后运行某些函数,那么就可以简化 Python 计时器的工作方式。其实,上下文管理器可以自动为计时时显式调用 .start()
和.stop()
。
同样,要让 Timer 作为上下文管理器工作,它需要遵守上下文管理器协议,换句话说,它必须实现 .__enter__()
和 .__exit__()
方法来启动和停止 Python 计时器。从目前的代码中可以看出,所有必要的功能其实都已经可用,因此只需将以下方法添加到之前编写的的 Timer
类中即可:
#?timer.py
@dataclass
class?Timer:
????#?其他代码保持不变
????def?__enter__(self):
????????"""Start?a?new?timer?as?a?context?manager"""
????????self.start()
????????return?self
????def?__exit__(self,?*exc_info):
????????"""Stop?the?context?manager?timer"""
????????self.stop()
Timer 现在就是一个上下文管理器。实现的重要部分是在进入上下文时, .__enter__()
调用 .start()
启动 Python 计时器,而在代码离开上下文时, .__exit__()
使用 .stop()
停止 Python 计时器。
from?timer?import?Timer
import?time
with?Timer():
????time.sleep(0.7)
Elapsed time: 0.7012 seconds
此处注意两个更微妙的细节:
.__enter__()
返回self
,Timer 实例,它允许用户使用as
将 Timer 实例绑定到变量。例如,使用with Timer() as t:
将创建指向 Timer 对象的变量t
。.__exit__()
需要三个参数,其中包含有关上下文执行期间发生的任何异常的信息。代码中,这些参数被打包到一个名为exc_info
的元组中,然后被忽略,此时 Timer 不会尝试任何异常处理。
在这种情况下不会处理任何异常。上下文管理器的一大特点是,无论上下文如何退出,都会确保调用.__exit__()
。在以下示例中,创建除零公式模拟异常查看代码功能:
from?timer?import?Timer
with?Timer():
????for?num?in?range(-3,?3):
????????print(f"1?/?{num}?=?{1?/?num:.3f}")
1 / -3 = -0.333
1 / -2 = -0.500
1 / -1 = -1.000
Elapsed time: 0.0001 seconds
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
ZeroDivisionError: division by zero
注意 ,即使代码抛出异常,Timer 也会打印出经过的时间。
使用 Python 定时器上下文管理器
现在我们将一起学习如何使用 Timer 上下文管理器来计时 "下载数据" 程序。回想一下之前是如何使用 Timer 的:
#?download_data.py
import?requests
from?timer?import?Timer
def?main():
????t?=?Timer()
????t.start()
????source_url?=?'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1'
????headers?=?{'User-Agent':?'Mozilla/5.0'}
????res?=?requests.get(source_url,?headers=headers)?
????t.stop()
????with?open('dataset/datasets.zip',?'wb')?as?f:
????????f.write(res.content)
if?__name__?==?"__main__":
????main()
我们正在对 requests.get()
的调用进行记时监控。使用上下文管理器可以使代码更短、更简单、更易读:
#?download_data.py
import?requests
from?timer?import?Timer
def?main():
????source_url?=?'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1'
????headers?=?{'User-Agent':?'Mozilla/5.0'}
????with?Timer():
????????res?=?requests.get(source_url,?headers=headers)
????????
????with?open('dataset/datasets.zip',?'wb')?as?f:
????????f.write(res.content)
if?__name__?==?"__main__":
????main()
此代码实际上与上面的代码相同。主要区别在于没有定义无关变量t
,在命名空间上无多余的东西。
写在最后
将上下文管理器功能添加到 Python 计时器类有几个优点:
省时省力:只需要一行额外的代码即可为代码块的执行计时。
可读性高:调用上下文管理器是可读的,你可以更清楚地可视化你正在计时的代码块。
使用 Timer
作为上下文管理器几乎与直接使用 .start()
和 .stop()
一样灵活,同时它的样板代码更少。
来源:https://mp.weixin.qq.com/s/Tx_AOjVCTWyGGZKz8fYs3g


猜你喜欢
- 引言本人因为种种原因(说来听听),放弃大学学的java,走上了golang这条路,本着干一行爱一行的情怀,做开发嘛,不能只会使用这门语言,所
- 前言:str转换为json格式,前提一定需要保证这个str的格式和json是一致的,即左边最外层是大括号,右边的最外层是大括号。如果不一致,
- Python自定义函数在Python编程中,可以使用已经定义好的函数,也可以自定义函数实现某些特殊的功能。自定义函数的语法在Python中,
- 如何显示数据库的结构?<html><head><meta http-equiv="Cont
- 标题:按某字段合并字符串之一(简单合并)描述:将如下形式的数据按id字段合并value字段。id val
- 这是我以前发表在经典论坛的帖子,现在转贴回来。仿淘宝网站的导航效果。此方法有几个优点:根据字数自适应项目长度不同的项目使用不同的颜色来区分无
- 前言之前在csdn上看见用python写春联的,这次突发奇想用python制作一张壁纸,其元素包括背景、文字、图片。知识点 用PIL创建一张
- 本文实例为大家分享了Linux下MySQL 5.6.27 安装教程,供大家参考,具体内容如下1、下载地址https://cdn.mysql.
- 通过win32 COM接口实现软件的操作本质上来看跟直接操作软件一致,这跟我之前经常用的通过各种扩展的组件或者库实现各种文件的处理有较大的差
- 目录一、路由配置二、vue页面嵌套三、嵌套联系一、路由配置const routes = [ { pat
- 我的同事Fara给大家介绍了戴尔网站首页的改版设计,这里我还想和大家介绍一下戴尔是如何从网站用户使用体验的角度进行设计,让大家进一步了解戴尔
- 01 ReplicaSet的架构 前面的文章中,我们说了ReplicaSet的基本概念和限制以及部署前的基本知识。
- 【引自ideras.me的博客】前言随着wordpress和静态网站的流行,markdown被用的越来越多。markdown是一个面向写作的
- 一、什么是域名系统DNS 计算机域名系统 (DNS) 是由解析器以及域名服务器组成的。当我们在上网的时候,通常输入的是网址,其实这就是一个域
- Jenkins定时构建时间设置Jenkins时区设置为北京时间打开 【系统管理】->【脚本命令行】运行下面的命令System.setP
- 一、文章主题在看到相关的抽奖诈骗报道,有的人却不明白是怎么回事。为了预防被抽奖诈骗,因此,我们通过一些简单的例子来说一说,抽奖更深层的逻辑,
- 一、python中对文件、文件夹操作时经常用到的os模块和shutil模块常用方法。1.得到当前工作目录,即当前Python脚本工作的目录路
- 1. 特定版本的python-opencv安装在https://www.lfd.uci.edu/~gohlke/pythonlibs/#op
- 有效地加载数据有时我们需大量地把数据加载到数据表,采用批量加载的方式比一个一个记录加载效率高,因为MySQL不用每加载一条记录就刷新一次索引
- 你不得不承认,今天网络发展之迅速,信息流动速度之快、量之大,是我们不曾考虑过的,但现在它就真真切切地摆在我们面前。如何接纳信息,怎么处理、消