写在前面


  • 有个简单的小需求,选择用​​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​​中这样描述:

优点
  1. 静态继承更灵活,与​对象的静态继承(多重继承)​相比, ​Decorator​模式提供了​更加灵活​​向对象添加职责​的方式。可以用添加和分离的方法,用装饰在运行时刻增加和删除职责。相比之下,继承机制要求为每个添加的职责创建一个新的子类(例如, ​​BorderscrollableTextView​​, ​​BorderedTextView​​ ),这会产生许多新的类,并且会增加系统的复杂度。此外,为一个特定的​​Component类​​提供多个不同的​​Decorator类​​,这就使得你可以对一些​​职责进行混合和匹配​​。使用Decorator模式可以很容易地​​重复添加一个特性​​,例如在TextView上添加双边框时,仅需将添加两个BorderDecorator即可。而两次继承Border类则极容易出错的.
  2. 避免在层次结构高层的类有太多的特征, Decorator模式提供了一种​“即用即付”的方法来添加职责​。它并不试图在一个复杂的可定制的类中支持所有可预见的特征,相反,你可以定义一个简单的类,并且用Decorator类给它逐渐地添加功能。可以​​从简单的部件组合出复杂的功能​​。这样,应用程序​​不必为不需要的特征付出代价​​。同时也更易于不依赖于Decorator扩展(甚至是不可预知的扩展)的类而独立地定义新类型的Decorator。扩展一个复杂类的时候,很可能会暴露与添加的职责无关的细节。
缺点
  1. ​Decorator​​​与​​Component​​​不一样, ​​Decorator​​​是一个​​透明的包装​​​。如果我们从​​对象标识​​​的观点出发,一个被装饰了的组件与这个组件是有差别的,因此,使用​​装饰时​​​不应该​​依赖对象标识​​。
  2. 有许多小对象采用Decorator模式进行系统设计,往往会产生许多看上去类似的小对象,这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统,排错也很困难。简单的讲,就是装饰器多了,容易混乱。

装饰器

Python装饰器的语法糖

​Python​​​给提供了​​一个装饰函数​​​更加简单的写法,语法糖的书写格式是: ​​@装饰器名字​​,通过语法糖的方式也可以完成对已有函数的装饰.

def check(fn):
print("装饰器函数")
def inner():
print("run....")
fn()
return inner

# 使用语法糖方式来装饰函数
@check
def comment():
print("函数执行suss")
# 运行
comment()

装饰器的场景

  • 实现函数执行时间的统计
  • 实现函数输出日志的功能

装饰带有不定长参数的函数

# 添加输出日志的功能
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

简单测试一下

@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]

脚本之外使用

.....
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] : [配置文件加载完成]