Python装饰器(计算函数运行花费时间)
python的装饰器和java的面向切面变成很相似,面向切面编程主要关心的是函数运行前(Before)和函数运行结束(After),可以针对函数前后做功能修复、功能插入工作,比如一些通用的打印日志,计算函数运行时间,拦截函数做一些事前处理, 相比而言python的装饰器更加灵活好用。
-
现在想要比较两个功能相同函数的性能,计算函数的运行时间
def parse_m3u8_file(m3u8_uri): pass # Before: 运行开始时间 # Running: 函数体运行中 # After: 运行结束时间 # 计算函数运行时间差并输出
完成这个功能,需要改写每个函数的函数体,并向上述描述添加重复代码,冗余而又乏味;有没有一种操作可以不用触及函数体的改动(或者微小改动)?装饰器可以优雅的完成这一功能。
import time def timefn1(fn): ''' :param fn: 被装饰器修饰的函数,将作为装饰器的默认参数传入 :return: 被装饰的函数本身 ''' def measure_use_time(*args, **kw): ''' 函数传参方式 fn(arg1, arg2, param1=a, param2=b) :param args: 位置参数(arg1, arg2, ....) 传参方式 fn(arg1, arg2, ...) :param kw: 字典参数{param1=a, param2=b, .....} 传参方式 fn(param1=a, param2=b, ....) :return: ''' t1 = time.time() res = fn(*args, **kw) t2 = time.time() print("@timefn: %s took %s" % (fn.__name__, t2-t1) return res return measure_use_time # 函数的定义上加@timefn1即可输出函数运行时间 ''' 形如: @timefn1 def parse_m3u8_file(m3u8_uri): pass '''
-
功能函数介绍:这个函数是对m3u8文件进行解析,提取其中的ts视频片段链接:
文件部分内容( 本文解析的m3u8文件附件):
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-KEY:METHOD=AES-128,URI="https://ts1.yuyuangewh.com:9999/20201006/a15C23PF/1000kb/hls/key.key" #EXTINF:4.28, https://ts1.yuyuangewh.com:9999/20201006/a15C23PF/1000kb/hls/nKw4m62o.ts #EXTINF:1.96, https://ts1.yuyuangewh.com:9999/20201006/a15C23PF/1000kb/hls/LFoLf2u7.ts #EXTINF:3.12, https://ts1.yuyuangewh.com:9999/20201006/a15C23PF/1000kb/hls/qhKKazou.ts #EXT-X-ENDLIST
函数代码部分:
def parse_m3u8_file(m3u8_uri): ''' 解析m3u8格式文件,得到ts视频片段链接 全部读取文件内容,进行正则匹配(ts链接URL) :param m3u8_uri: 本地缓存的m3u8文件路径 :return: ''' with open(m3u8_uri, 'r') as fr: data = fr.read() ts_pattern = r"https://.*\.ts" ts_urls = re.findall(ts_pattern, data) return ts_urls def parse_m3u8(m3u8_uri): ''' 解析m3u8格式文件,得到ts视频片段链接 逐行读取文件内容,添加ts链接URL(不以#开头) :param m3u8_uri: 本地缓存的m3u8文件路径 :return: ''' ts_urls = [] fr = open(m3u8_uri, 'r') while True: buf = fr.readline() if not buf: print(m3u8_uri + ' has read...') break if not buf.startswith('#'): ts_urls.append(buf.strip()) fr.close() return ts_urls
-
装饰器改进:
如果你现在想给装饰器提供参数来简要说明一下函数的用法(或者其他用途的参数),这个fn(被装饰器修饰的函数)不是不能作为默认参数传入了?python提供了解决方案(funtools.wrap来装饰函数):from functools import wraps import logging def timefn2(fn_use=''): ''' :param fn_use: 装饰器的参数 ''' def wrapper(fn): @wraps(fn) #lookat here def measure_use_time(*args, **kw): ''' 函数传参方式 fn(arg1, arg2, param1=a, param2=b) :param args: 位置参数(arg1, arg2, ....) 传参方式 fn(arg1, arg2, ...) :param kw: 字典参数{param1=a, param2=b, .....} 传参方式 fn(param1=a, param2=b, ....) :return: ''' t1 = time.time() res = fn(*args, **kw) t2 = time.time() logging.warning("@timefn: %s(%s) took %s" % (fn.__name__, fn_use, format_use_time(t2 - t1))) return res return measure_use_time return wrapper # 性能计算功能的添加需要加入装饰器@timefn2 ''' 形如: # 不带函数用途的写法 @timefn2() def parse_m3u8_file(m3u8_uri): pass # 带函数用途的写法 @timefn2('parse2') def parse_m3u8(m3u8_uri): pass '''
注意到上面使用到了一个函数format_use_time是将秒数转化成时分秒的格式,方便查看; 第二个引入了日志模块,不在使用print来进行控制台输出:
def format_use_time(seconds): ''' 将秒数转化为 HH:MM:SS格式 :param seconds: :return: 格式化字符串 ''' """ # 其他语言通用写法 h = int(seconds/3600) seconds_ = seconds % 3600 m = int(seconds_/60) s = seconds_ % 60 return "{:0>2d}:{:0>2d}:{:0>9.6f}".format(h, m, s) """ m, s = divmod(seconds, 60) h, m = divmod(m, 60) return "{:0>2d}:{:0>2d}:{:0>9.6f}".format(int(h), int(m), s)
优化上面的装饰器函数,加入异常处理机制:
from functools import wraps import logging def timefn(fn_use=''): ''' :param fn_use: 装饰器的参数 ''' def wrapper(fn): @wraps(fn) def measure_use_time(*args, **kw): ''' 函数传参方式 fn(arg1, arg2, param1=a, param2=b) :param args: 位置参数(arg1, arg2, ....) 传参方式 fn(arg1, arg2, ...) :param kw: 字典参数{param1=a, param2=b, .....} 传参方式 fn(param1=a, param2=b, ....) :return: ''' t1 = time.time() try: res = fn(*args, **kw) except Exception as e: logging.error(f"@timefn %s(%s) execute error" % (fn.__name__, fn_use)) # decide whether throw exception or not (决定受否抛出异常,终止程序) # raise e return None else: t2 = time.time() logging.warning("@timefn: %s(%s) took %s" % (fn.__name__, fn_use, format_use_time(t2 - t1))) return res return measure_use_time return wrapper