我们在上面创建了第一个Python定时器类,然后逐渐扩展我们的Timer类,它的代码也是丰富而强大。我们不能就此打住,我们还需要模板化一些代码来使用Timer:首先,实例化类其次,在要计时的代码块之前调用.start()最后,在要计时的代码块之后调用.stop()编写Python计时器上下文管理器Python有一个独特的结构,用于在代码块之前和之后调用函数:上下文管理器。了解Python中的上下文管理器上下文管理器长期以来一直是Python的重要组成部分。由PEP343于2005年引入,并首先在Python2.5中实现。可以使用with关键字在代码中识别上下文管理器:withEXPRESSIONasVARIABLE:BLOCKEXPRESSION是一些返回上下文管理器的Python表达式。首先,上下文管理器绑定到变量名VARIABLE,而BLOCK可以是任何常规的Python代码块。上下文管理器确保程序在执行BLOCK之前调用一些代码,在执行BLOCK之后调用一些其他代码。这样即使BLOCK抛出异常,后面的还是会被执行。上下文管理器最常见的用途是处理不同的资源,例如文件、锁和数据库连接。上下文管理器用于资源使用后的释放和清理。以下示例通过简单地打印包含冒号的行来演示timer.py的基本结构。此外,它还演示了在Python中打开文件的常用习惯用法:withopen("timer.py")asfp:print("".join(lnforlninfpif":"inln))classTimerError(Exception):classTimer:timers:ClassVar[Dict[str,float]]={}name:Optional[str]=Nonetext:str="Elapsedtime:{:0.4f}seconds"记录器:Optional[Callable[[str],None]]=print_start_time:Optional[float]=field(default=None,init=False,repr=False)def__post_init__(self)->None:如果self.name不是None:defstart(self)->None:如果self._start_time不是None:defstop(self)->float:如果self._start_time是None:如果self.logger:如果self.name:注意,使用open()作为上下文管理器,文件指针fp不会被显式关闭,可以确认fp自动关闭:fp.closedTrue在这个例子中,open("timer.py")是一个返回上下文管理器的表达式。此上下文管理器绑定到名称fp。上下文管理器在print()执行期间有效。这个单行代码块在fp的上下文中执行。fp是上下文管理器是什么意思?从技术上讲,fp实现了上下文管理器协议。Python语言底层有许多不同的协议。协议可以被认为是一份合同,规定了我们的代码必须实现哪些特定方法。上下文管理器协议包含两个方法:.__enter__()在进入与上下文管理器关联的上下文时调用。.__exit__()在与上下文管理器关联的上下文退出时调用。换句话说,要自己创建上下文管理器,您需要编写一个实现.__enter__()和.__exit__()的类。试试你好,世界!上下文管理器示例:#studio.pyclassStudio:def__init__(self,name):self.name=namedef__enter__(self):print(f"Hello{self.name}")returnselfdef__exit__(self,exc_type,exc_value,exc_tb):print(f"Seeyoulater,{self.name}")Studio是contextmanager,它实现了contextmanager协议,使用方法如下:fromstudioimportStudiowithStudio("Yunduojun"):print("忙...")你好云多君很忙...等会儿见,云多君首先注意.__enter__()是在做某事之前调用的,.__exit__()是在做某事之后调用的。在此示例中,没有对上下文管理器的引用,因此无需使用as来命名上下文管理器。接下来,注意self.__enter__()的返回值受as约束。创建上下文管理器时,您通常希望从.__enter__()返回self。返回值可以这样使用:fromgreeterimportGreeterwithGreeter("Yunduojun")asgrt:print(f"{grt.name}isbusy...")HelloYunduojunYunduojunisbusy...待会见,云朵君在编写__exit__函数时需要注意。它必须有这三个参数:exc_type:异常类型exc_val:异常值exc_tb:异常错误堆栈信息这三个参数用于编译器中的上下文管理错误处理,它们作为sys.exc_info()的返回值返回。当主逻辑代码没有报异常时,这三个参数都会为None。如果在执行块时发生异常,代码将使用异常类型、异常实例和回溯对象(即exc_type、exc_value和exc_tb)调用.__exit__()。通常,这些在上下文管理器中会被忽略,并在引发异常之前调用.__exit__():fromgreeterimportGreeterwithGreeter("Yunduojun")asgrt:print(f"{grt.age}doesnotexist")Hello,云朵老师,云朵老师回头见(最近调用last):File"",line2,inAttributeError:'Greeter'objecthasnoattribute'age'可见,即使代码有错误,还是会打印“云朵先生,待会见”。了解和使用contextlib现在我们对什么是上下文管理器以及如何创建自己的上下文管理器有了基本的了解。在上面的示例中,我们只是编写了一个类来构建上下文管理器。如果只想实现一个简单的功能,写一个类就有点太复杂了。这时候我们就想,要是能只写一个函数来实现上下文管理器就好了。Python早就想到了这一点。它为我们提供了一个装饰器,只要你按照它的代码协议实现功能内容,就可以把这个功能对象变成一个上下文管理器。我们根据contextlib协议自己实现了一个上下文管理器。为了更直观,我们改变用例,创建一个我们常用和熟悉的上下文管理器(withopen)。importcontextlib@contextlib.contextmanagerdefopen_func(file_name):#__enter__方法print('openfile:',file_name,'in__enter__')file_handler=open(file_name,'r')#[key]:yieldyieldfile_handler#__exit__Methodprint('closefile:',file_name,'in__exit__')file_handler.close()returnwithopen_func('test.txt')asfile_in:forlineinfile_in:print(line)修饰函数中,必须是生成器(withyield),yield之前的代码相当于__enter__中的内容。yield之后的代码相当于__exit__中的内容。以上代码只能实现上下文管理器的第一个目的(管理资源),不能实现第二个目的(处理异常)。如果要处理异常,可以改成下面这样。importcontextlib@contextlib.contextmanagerdefopen_func(file_name):#__enter__方法print('openfile:',file_name,'in__enter__')file_handler=open(file_name,'r')try:yieldfile_handlerexceptExceptionasexc:#deal异常打印('异常被抛出')最后:打印('关闭文件:',file_name,'in__exit__')file_handler.close()返回open_func('test.txt')asfile_in:forlineinfile_in:1/0print(line)Python标准库中的contextlib包括用于定义新上下文管理器的便捷方法,以及可用于关闭对象、抑制错误甚至什么都不做的现成上下文管理器!创建Python计时器上下文管理器现在您已经了解了上下文管理器的一般工作方式,它们如何帮助计时代码?假设地,如果您可以在代码块之前和之后运行某些函数,则可以简化Python计时器的工作方式。事实上,上下文管理器可以自动显式调用.start()和.stop()进行计时。同样,要使Timer作为上下文管理器工作,它需要遵守上下文管理器协议,换句话说,它必须实现.__enter__()和.__exit__()方法来启动和停止Python计时器。从目前的代码可以看出,所有需要的功能都已经具备了,所以只需要在之前编写的Timer类中添加如下方法即可:#timer.py@dataclassclassTimer:#其他代码不变def__enter__(self):"""作为上下文管理器启动一个新计时器"""self.start()returnselfdef__exit__(self,*exc_info):"""停止上下文管理器定时器"""self.stop()Timer现在是上下文管理器。实现的重要部分是.__enter__()在进入上下文时调用.start()来启动Python计时器,而.__exit__()在代码离开上下文时使用.stop()来停止Python计时器。fromtimerimportTimerimporttimewithTimer():time.sleep(0.7)Elapsedtime:0.7012seconds这里要注意两个更细微的细节:.__enter__()returnself,Timer实例,允许用户作为Timer使用实例绑定到变量。例如,使用withTimer()ast:将创建一个指向Timer对象的变量t。.__exit__()采用三个参数,其中包含有关上下文执行期间发生的任何异常的信息。在代码中,这些参数被打包成一个名为exc_info的元组,然后被忽略,此时Timer不会尝试任何异常处理。在这种情况下不处理任何异常。上下文管理器的一个重要特性是,无论上下文如何退出,它们都会确保调用.__exit__()。在下面的例子中,创建了一个除零公式来模拟异常查看代码函数:fromtimerimportTimerwithTimer():fornuminrange(-3,3):print(f"1/{num}={1/num:.3f}")1/-3=-0.3331/-2=-0.5001/-1=-1.000Elapsedtime:0.0001secondsTraceback(mostrecentcalllast):File"",line3、在ZeroDivisionError:divisionbyzero注意,即使代码抛出异常,Timer也会打印经过的时间。使用PythonTimerContextManager现在我们将一起学习如何使用TimerContextManager为“下载数据”程序计时。回忆一下之前Timer的使用方式:#download_data.pyimportrequestsfromtimerimportTimerdefmain():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()withopen('dataset/datasets.zip','wb')asf:f.write(res.content)if__name__=="__main__":main()我们正在定时监控对requests.get()的调用。使用上下文管理器可以使代码更短、更简单、更易读:#download_data.pyimportrequestsfromtimerimportTimerdefmain():source_url='https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1'headers={'User-Agent':'Mozilla/5.0'}withTimer():res=requests.get(source_url,headers=headers)withopen('dataset/datasets.zip','wb')asf:f.write(res.content)if__name__=="__main__":main()这段代码其实和上面那段是一样的。主要区别在于没有定义不相关的变量t,命名空间中也没有多余的东西。将上下文管理器功能添加到Python定时器类有几个优点:节省时间和精力:只需要一行额外的代码来为代码块的执行计时。高可读性:调用上下文管理器是可读的,你可以更清楚地看到你正在计时的代码块。使用Timer作为上下文管理器几乎与直接使用.start()和.stop()一样灵活,同时样板代码更少。在本系列的下一篇文章中,云朵君将和大家一起学习如何将Timer作为装饰器使用在代码中,从而更容易监控代码的完整运行过程。让我们一起期待吧!