Python小脚本基于装饰器的函数日志脚本

Posted 山河已无恙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python小脚本基于装饰器的函数日志脚本相关的知识,希望对你有一定的参考价值。

写在前面


  • 有个简单的小需求,选择用pythoh实现

  • 有些打印方法业务日志,参数执行时间的语句感觉有些冗余

  • 所以想用类似AOP的方式实现

  • 利用python闭包函数实现的装饰器及提供的语法糖可以简单实现。

  • 博文内容包括两部分:

    • Python闭包&装饰器装饰器设计模式简述
    • 基于Python装饰器函数日志模块实现:
    • 日志提供函数执行时间入参,函数业务信息的采集
    • 日志位置支持函数前函数最终函数异常时环绕采集四种方式
  • 理解错误的地方请小伙伴批评指正

我只是怕某天死了,我的生命却一无所有。----《奇幻之旅》


理论准备

在介绍脚本前,我们简单介绍下用到的知识点

闭包

在一般的编程语言中,比如Java,C,C++,C#,Golang中,我们知道一个函数调用完,函数内定义的变量都销毁了,有时候需要保存函数内的这些变量,在这些变量的基础上完成一些操作。我们只能通过返回值的方式来处理

在一些解释型的语言中,比如JSPython等,我们可以通过函数嵌套的方式,可以获取函数内部的一些变量信息。这个行为,我们称为闭包
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. DecoratorComponent不一样, 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] : :.1fs, [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] : :.1fs,[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] : :.1fs, [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] : [配置文件加载完成]        

以上是关于Python小脚本基于装饰器的函数日志脚本的主要内容,如果未能解决你的问题,请参考以下文章

装饰器

python装饰器的小细节

如何在 python 中获取传递给我的装饰器的函数的文件名?

python的装饰器

python 装饰器的使用

Python学习---装饰器的学习1210