写在前面
- 有个简单的小需求,选择用
pythoh
实现 - 有些打印方法
业务日志
,参数
,执行时间
的语句感觉有些冗余 - 所以想用类似
AOP
的方式实现 - 利用
python
里闭包函数
实现的装饰器
及提供的语法糖
可以简单实现。 - 博文内容包括两部分:
Python闭包&装饰器
,装饰器设计模式
简述- 基于
Python装饰器
的函数日志模块
实现: - 日志提供函数
执行时间
,入参
,函数业务信息
的采集 - 日志位置支持
函数前
,函数最终
,函数异常时
,环绕采集
四种方式
- 理解错误的地方请小伙伴批评指正
「 我只是怕某天死了,我的生命却一无所有。----《奇幻之旅》」
理论准备
在介绍脚本前,我们简单介绍下用到的知识点
闭包
在一般的编程语言中,比如Java
,C
,C
,C#
中,我们知道一个函数调用完,函数内定义的变量都销毁了,有时候需要保存函数内的这些变量,在这些变量的基础上完成一些操作。我们只能通过返回值的方式来处理
在一些解释型
的语言中,比如JS
,Python
等,我们可以通过函数嵌套
的方式,可以获取函数内部的一些变量信息。这个行为,我们称为闭包JavaScript中的使用
// 定义一个外部函数
function outer(num1){
let name = 'liruilong'
// 定义一个内部函数
function inner(num2){
// 内部函数使用了外部函数的变量(num1)
console.log(num1 num2)
}
// 外部函数返回了内部函数,这里返回的内部函数就是闭包
return inner()
}
f = outer(1)
f(2)
Python中的使用
def func_out(num1):
def func_inner(num2):
# 内部函数使用了外部函数的变量(num1)
result = num1 num2
print("结果是:", num1 num2)
# 外部函数返回了内部函数,这里返回的内部函数就是闭包
return func_inner
# 创建闭包实例
f = func_out(1)
# 执行闭包
f(2)
f(3)
「闭包的定义:
在函数嵌套
的前提下,内部函数使用了外部函数的变量
,并且外部函数返回了内部函数
,我们把这个使用外部函数变量的内部函数
称为闭包
。」
闭包的构成条件
通过闭包的定义,我们可以得知闭包的形成条件:
- 在函数嵌套(函数里面再定义函数)的前提下
- 内部函数使用了外部函数的变量(还包括外部函数的参数)
- 外部函数返回了内部函数
闭包的作用
闭包可以保存外部函数内的变量,不会随着外部函数调用完而销毁。同时,由于闭包引用了外部函数的变量,则外部函数的变量没有及时释放,消耗内存。
闭包的使用
闭包可以提高代码的可重用性,不需要再手动定义额外的功能函数。
- 闭包可以实现
python装饰器
,关于装饰器简单讲就是给已有函数增加额外功能的函数,它本质上就是一个闭包函数。
,当然python
也可以实现基于类的装饰器
装饰器的功能特点:
- 不修改已有函数的源代码
- 不修改已有函数的调用方式
- 给已有函数增加额外的功能
- 闭包函数有且
只有一个参数,必须是函数类型
,这样定义的函数才是装饰器
。
为什么叫装饰器
,这里我们简单讲讲面向对象中对象结构型
设计模式装饰器设计模式
,以及六大面向对象设计原则
之一开闭原则(Open Close Principle)
关于装饰器设计模式的定义
:即动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案
。遵循开闭原则
,对扩展开放,对修改关闭。
关于装饰器设计模式的优点和缺点,GOF
中这样描述:
优点
- 「比
静态继承
更灵活,与对象的静态继承(多重继承)
相比,Decorator
模式提供了更加灵活
的向对象添加职责
的方式」。可以用添加和分离的方法,用装饰在运行时刻增加和删除职责。相比之下,继承机制要求为每个添加的职责创建一个新的子类(例如,BorderscrollableTextView
,BorderedTextView
),这会产生许多新的类,并且会增加系统的复杂度。此外,为一个特定的Component类
提供多个不同的Decorator类
,这就使得你可以对一些职责进行混合和匹配
。使用Decorator模式可以很容易地重复添加一个特性
,例如在TextView上添加双边框时,仅需将添加两个BorderDecorator即可。而两次继承Border类则极容易出错的. - 「避免在
层次结构高层的类
有太多的特征, Decorator模式提供了一种“即用即付”的方法来添加职责
」 。它并不试图在一个复杂的可定制的类中支持所有可预见的特征,相反,你可以定义一个简单的类,并且用Decorator类给它逐渐地添加功能。可以从简单的部件组合出复杂的功能
。这样,应用程序不必为不需要的特征付出代价
。同时也更易于不依赖于Decorator扩展(甚至是不可预知的扩展)的类而独立地定义新类型的Decorator。扩展一个复杂类的时候,很可能会暴露与添加的职责无关的细节。
缺点
Decorator
与Component
不一样,Decorator
是一个透明的包装
。如果我们从对象标识
的观点出发,一个被装饰了的组件与这个组件是有差别的,因此,使用装饰时
不应该依赖对象标识
。- 有许多小对象采用Decorator模式进行系统设计,往往会产生许多看上去类似的小对象,这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统,排错也很困难。简单的讲,就是装饰器多了,容易混乱。
装饰器
Python装饰器的语法糖
Python
给提供了一个装饰函数
更加简单的写法,语法糖的书写格式是: @装饰器名字
,通过语法糖的方式也可以完成对已有函数的装饰.
def check(fn):
print("装饰器函数")
def inner():
print("run....")
fn()
return inner
# 使用语法糖方式来装饰函数
@check
def comment():
print("函数执行suss")
# 运行
comment()
装饰器的场景
- 实现函数执行时间的统计
- 实现函数输出日志的功能
装饰带有不定长参数的函数
代码语言:javascript复制# 添加输出日志的功能
def logging(fn):
def inner(*args, **kwargs):
print("--正在努力计算--")
fn(*args, **kwargs)
return inner
# 使用语法糖装饰函数
@logging
def sum_num(*args, **kwargs):
result = 0
for value in args:
result = value
for value in kwargs.values():
result = value
print(result)
sum_num(1, 2, a=10)
========================
>--正在努力计算--
13
多个装饰器的使用
多个装饰器的装饰过程是: 离函数最近的装饰器先装饰,然后外面的装饰器再进行装饰,由内到外的装饰过程
def make_div(func):
"""对被装饰的函数的返回值 div标签"""
def inner(*args, **kwargs):
return "<div>" func() "</div>"
return inner
def make_p(func):
"""对被装饰的函数的返回值 p标签"""
def inner(*args, **kwargs):
return "<p>" func() "</p>"
return inner
# 装饰过程:
# 1 content = make_p(content)
# 2 content = make_div(content)
# content = make_div(make_p(content))
@make_div
@make_p
def content():
return "人生苦短"
print(content())
============
<div><p>人生苦短</p></div>
带有参数的装饰器
带有参数的装饰器就是使用装饰器装饰函数的时候可以传入指定参数,语法格式: @装饰器(参数,...)
# 添加输出日志的功能
def logging(flag):
def decorator(fn):
def inner(num1, num2):
if flag == " ":
print("--正在努力加法计算--")
elif flag == "-":
print("--正在努力减法计算--")
result = fn(num1, num2)
return result
return inner
# 返回装饰器
return decorator
# 使用装饰器装饰函数
@logging(" ")
def add(a, b):
result = a b
return result
@logging("-")
def sub(a, b):
result = a - b
return result
print(add(1, 2))
print(sub(1, 2))
类装饰器的使用
装饰器还有一种特殊的用法就是类装饰器,就是通过定义一个类来装饰函数。
class Check(object):
def __init__(self, fn):
# 初始化操作在此完成
self.__fn = fn
# 实现__call__方法,表示对象是一个可调用对象,可以像调用函数一样进行调用。
def __call__(self, *args, **kwargs):
# 添加装饰功能
print("请先登陆...")
self.__fn()
@Check
def comment():
print("发表评论")
comment()
==============
请先登陆...
发表评论
@Check 等价于 comment = Check(comment)
, 所以需要提供一个init
方法,并多增加一个fn参数
。- 要想类的
实例对象
能够像函数一样调用,需要在类里面使用call
方法,把类的实例变成可调用对象(callable),也就是说可以像调用函数一样进行调用。``在call方法里进行对fn函数的装饰,可以添加额外的功能。
具体的脚本
基于装饰器函数日志脚本
讲了这么多,我们来看看,如何在用装饰器实现函数的日志
这里需要注意一下@functools.wraps(func)
这个装饰器,一般函数被装饰器装饰完之后,被装饰的函数的名字会变成装饰器函数,通过该装饰器,我们可以打印实际的函数名。
log_decorator.py
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : log_decorator.py
@Time : 2022/03/22 10:24:51
@Author : Li Ruilong
@Version : 1.0
@Contact : 1224965096@qq.com
@Desc : 方法日志装饰类
"""
# here put the import lib
import functools
import time
import logging
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(levelname)s: %(message)s')
def method_before(message="before message default"):
"""
@Time : 2022/03/22 11:01:46
@Author : Li Ruilong
@Version : 1.0
@Desc : 前置日志:方法执行前输出的日志
"""
def method_logging(func):
# 用于获取原来的函数名
@functools.wraps(func)
def wrapper(*args, **kw):
logging.info('[method] : [{}] , [param] : [{}],[message] : [{}],'.format(
func.__name__,args, message))
return func(*args, **kw)
return wrapper
return method_logging
def method_after(message="after message default"):
"""
@Time : 2022/03/22 16:01:21
@Author : Li Ruilong
@Version : 1.0
@Desc : 最终日志:不管方法是否执行成功,执行后都会输出的日志
"""
def method_logging(func):
@functools.wraps(func)
def wrapper(*args, **kw):
start = time.time()
try:
return func(*args, **kw)
finally:
logging.info('[method] : [{}] , [cost] : {:.1f}s, [param] : [{}],[message] : [{}],'.format(
func.__name__, time.time() - start, args, message))
return wrapper
return method_logging
def method_around(before="Before message default", afterReturning="AfterReturning message default"):
"""
@Time : 2022/03/22 11:09:24
@Author : Li Ruilong
@Version : 1.0
@Desc : 环绕日志:方法执行前后输出的日志
"""
def method_logging(func):
@functools.wraps(func)
def wrapper(*args, **kw):
start = time.time()
try:
logging.info('[method] : [{}] , [param] : [{}],[message] : [{}]'.format(
func.__name__, args, before))
return func(*args, **kw)
except Exception as e:
logging.error(e)
finally:
logging.info('[method] : [{}] , [cost] : {:.1f}s,[message] : [{}]'.format(
func.__name__, time.time() - start, afterReturning))
return wrapper
return method_logging
def method_after_throwing(message="After-Throwing message default"):
"""
@Time : 2022/03/22 11:37:56
@Author : Li Ruilong
@Version : 1.0
@Desc : 异常日志,方法执行异常后输出的日志
"""
def method_logging(func):
@functools.wraps(func)
def wrapper(*args, **kw):
start = time.time()
try:
return func(*args, **kw)
except Exception as e:
logging.error('[method] : [{}] , [cost] : {:.1f}s, [param] : [{}],[message] : [{}],,except[{}]'.format(
func.__name__, time.time() - start, args, message, e))
return wrapper
return method_logging
简单测试一下
代码语言:javascript复制@method_before("前置内容")
def __method_before_test(a='www', b=1, c=[1, 2]):
time.sleep(2)
print( "前置函数")
@method_around("前置内容", "后置内容")
def __method_around_test(a='www', b=1, c=[1, 2]):
time.sleep(3)
print( "环绕函数")
@method_after_throwing("异常日志内容")
def __method_after_throwing_test(a='www', b=1, c=[1, 2]):
time.sleep(3)
print( "异常函数")
raise
if __name__ == "__main__":
print(__method_before_test(1, 'hello', c=[5, 6]))
print(__method_around_test(1, 'hello', c=[5, 6]))
print(__method_after_throwing_test(1, 'hello', c=[5, 6]))
==============================
2022-04-01 15:00:09,888 - INFO: [method] : [__method_before_test] , [param] : [(1, 'hello')],[message] : [前置内容],
前置函数
2022-04-01 15:00:11,891 - INFO: [method] : [__method_around_test] , [param] : [(1, 'hello')],[message] : [前置内容]
环绕函数
2022-04-01 15:00:14,894 - INFO: [method] : [__method_around_test] , [cost] : 3.0s,[message] : [后置内容]
异常函数
2022-04-01 15:00:17,898 - ERROR: [method] : [__method_after_throwing_test] , [cost] : 3.0s, [param] : [(1, 'hello')],[message] : [异常日志内容],,except[No active exception to reraise]
脚本之外使用
代码语言:javascript复制
.....
import log_decorator as log
....
@log.method_around("开始加载配置文件", "配置文件加载完成")
def __init__(self, file_name="config.yaml"):
config_temp = None
try:
# 获取当前脚本所在文件夹路径
cur_path = os.path.dirname(os.path.realpath(__file__))
# 获取yaml文件路径
yaml_path = os.path.join(cur_path, file_name)
f = open(yaml_path, 'r', encoding='utf-8')
config_temp = f.read()
except Exception as e:
logging.info("配置文件加载失败", e)
finally:
f.close()
self._config = yaml.safe_load(config_temp) # 用load方法转化
========================
2022-04-01 19:16:53,175 - INFO: [method] : [__init__] , [param] : [(<__main__.Yaml object at 0x01482118>,)],[message] : [开始加载配置文件]
2022-04-01 19:16:53,184 - INFO: [method] : [__init__] , [cost] : 0.0s,[message] : [配置文件加载完成]