python装饰器实战

装饰器简单介绍

  • 装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。下面看个简单的例子:
@decorate
def target():
    pass
  • 其实就是:
target = decorate(target)
  • 装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行
  • 也就是说,只要你给某个函数新增了装饰器,这个函数就已经通过自由变量的方式传入给了装饰器函数,这个函数内存指向了装饰器所在的函数。这里的加载时你可以理解为导入时,这样就可以在函数运行前做一些其他的操作。下面通过单例模式简单分析下装饰器。

装饰器之单例模式分析

  • 所谓单例模式,简单来说就是类的实例只能存在一个。下面直接看代码进行分析:
from functools import wraps

def Singleton(cls):  # 传入类(cls)而不是实例
    """单例模式之装饰器实现"""
    instance_dict = {}  # 使用字典存储类的实例

    @wraps(cls)  # 消除被装饰函数内置属性和方法被替换的影响
    def wrapper(*args, **kwargs):  # 解包传入参数
        if cls not in instance_dict: 
            # 如果类 cls 不在字典 instance_dict 的 key 中,则调用类cls构造方法新建实例
            instance_dict[cls] = cls(*args, **kwargs)
        return instance_dict[cls]  # 返回类cls的实例

    return wrapper  # 返回闭包函数
  • 通过代码逐行分析应该很清楚,只要给某个类添加了装饰器,这个类在初始化时直接进入装饰器中的闭包函数返回类的唯一实例。
  • 但是这个装饰器还有个显而易见的问题:线程不安全。当有多个线程同时去获取这个单例资源时,装饰器是不会对线程进行限制的。解决方法也很简单,给装饰器中的闭包函数加锁即可,也就是说,该资源只能被一个线程访问,只有该线程释放了锁,其他线程才能访问。如下示例
import threading
from functools import wraps


def synchronized(func):
    '''线程锁装饰器'''
    func.__lock__ = threading.Lock()

    def syn_func(*args, **kwargs):
        with func.__lock__:
            return func(*args, **kwargs)

    return syn_func


def Singleton(cls):
    instance_dict = {}

    @synchronized
    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in instance_dict:
            instance_dict[cls] = cls(*args, **kwargs)
        return instance_dict[cls]

    return wrapper


@Singleton
class Foo:
    pass


if __name__ == '__main__':
    f1 = Foo()
    f2 = Foo()
    print(f1 is f2)  # True
  • 给wrapper闭包函数加锁,这样无论有多少线程,只要没有获取锁资源,都会进行等待,直至上一个线程释放锁,这样就解决了多线程下资源的安全获取。
  • 单例模式是个经典的例子,下面步入正题,说说在实际项目中如何使用装饰器

使用装饰器进行参数校验

  • 在项目开发中进行参数校验是个再正常不过得事情,毕竟我们无法保证前端传入正确的数据。比如在 fastapi 中最常使用的就是 pydantic 库,这个库十分强大,使用起来也很简单。但是这不是我们讨论的主题,现在说下该如何使用装饰器对传入的参数进行校验。
  • 比如你想对传入的参数进行非空校验,但是又不想在函数里面调用其他函数进行参数处理,使用装饰器就可以很优雅的实现,这其实就是设计模式的思想,AOP就是这样做的。下面直接看代码:
def para_none_check(func):
    """参数非空校验"""

    @wraps(func)
    def none_check(data):
        if not data:
            raise TypeError("传入参数不能为空")
        return func(data)

    return none_check
  • 下面来简单测试下
@para_none_check
def func_test(data):
    print("func_test running")
   

if __name__ == '__main__':
    try:
        func_test([])
    except TypeError as e:
        print(e)  # 传入参数不能为空
  • 可知,当传入参数为空时抛出 TypeError 错误,如果不想抛出错误也可以,直接返回错误提示也可以,比如把 raise TypeError("传入参数不能为空") 替换为 return {"code": "01", "error": "传入参数不能为空"}
  • 如果我想传入指定的关键字参数,这又该如果做呢?比如我想传入的参数里面必须包含 name和 age。你可能第一时间想到这样限定:
def func(name, age, **kwargs):
  • 但是实际情况很多时候往往无法直接获取 name 和 age,因为数据是从前端获取的,我们只能拿到传入的数据,并不知道这些数据是不是包含这些字段。不过有了前面的的基础,现在对参数进行校验也就不难了,示例如下:
def para_verification(required_fields: list):  # 指定要传入的参数字段列表
    """参数校验,必须传入装饰器指定的参数"""

    def decorate(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            para = list(kwargs.keys())  # 获取传入的全部参数字段列表
            for field in required_fields:
                if field not in para:
                    return {"code": "01", "error": "传入参数有误", "required_fields": required_fields, 'kwargs': kwargs}
            return func(*args, **kwargs)

        return wrapper

    return decorate
  • 如果难以理解的话先来看个测试示例:
@para_verification(['name', 'age'])  # 指定func_test必须传入name和age
def func_test(*args, **kwargs):
    print("func_test running")
    return True


if __name__ == '__main__':
    kw1 = {"name": "张三", "height": 175}
    kw2 = {"name": "张三", "height": 175, "age": 18}
    print(func_test(**kw1))
    # {'code': '01', 'error': '传入参数有误', 'required_fields': ['name', 'age'], 'kwargs': {'name': '张三', 'height': 175}}
    print(func_test(**kw2))
    # func_test running
    # True
  • 通过示例得出只要在装饰器参数中指定要传入的字段列表,实际运行函数的时候会先用装饰器中的参数和传入的参数进行校验,如果指定要传入的参数都存在,则校验通过,直接运行该函数。很多框架都利用了这样的思想。
  • 当然,参数校验可不是这么简单的事情,这里只是做了初步分析,实际场景可能远远比这复杂,但是万变不离其中,你只要理解了装饰器的原理,编码自然水到渠成。
  • 说了这么多只是为了让你对装饰器有更深的体会。而且举出的实例都是很可能在实际项目中使用的,而且可以直接拿来使用。下面讲讲如何利用装饰器以及一些模块进行内存占用检测。

接口内存泄露检测

  • 可能有人会问,python作为一种动态语言,也会存在内存泄露吗?pyhton确实不容易发生内存泄露,但是并不表示不会发生。循环引用就是一个典型的例子,python解释器会在对象的引用计数归零时删除该对象。
  • 那到底什么是引用计数呢?比如a = [1, 2],给 a 赋值了一个列表对象,那么 a 就指向了这个列表的内存地址,这称为强引用,因为弱应用不太常见,所以强引用一般就称为引用,如果我们再赋值 b = a, 这样列表对象又有了一个引用,引用计数为2。如果我们这时候删除a:del a,这样其实是删除了变量 a 对列表对象的引用,并没有直接删除列表这个对象。
  • 简单来说,就是所有指向对象的变量都不在存在后才会去删除这个对象,接着上面,给b重新赋值改变b的内存指向:b = [1, 4],这时候列表对象 [1,2] 没有任何变量指向它,引用计数归零,该对象被gc删除回收内存。
  • 简单了解了python 的垃圾回收机制,我们也就知道为什么会出现内存泄露了:只要一个对象的引用计数没有归零,那么这个对象就不会被删除。但是实际项目中难的不是如何解决内存泄露,而是如何检测出哪里发生了内存泄露。
  • 检测内存泄露的方式有很多,比如使用pyrasite库进行远程检测。其实内存检测是提前设置的,我们只需要在接口上添加装饰器,然后在装饰器中统计接口运行时各个对象的占用内存或者哪些文件的第几行代码占用内存。这里使用python3 内置的 tracemalloc 库就可以了。
  • 先说下tracemalloc 的用法,如下示例:


  • 打印结果如下


  • 可以看到第40行占用了3532kb内存,这样就能知道哪行代码存在性能问题从而进行优化。
  • 也可以在不同地方新建快照,比较代码运行后的性能差异:


  • 测试打印结果


  • 可以看到占到用较高的就只有第40行。
  • 学会了tracemalloc 的基本使用,接下来使用装饰器来实现:
import tracemalloc as tc
from functools import wraps
from loguru import logger

def interface_memory_leak_check(func):
    """接口内存占用检测"""

    @wraps(func)
    def wrapper(*args, **kwargs):
        tc.start()  # 开始跟踪内存分配
        snapshot1 = tc.take_snapshot()  # 建立快照
        re_data = func(*args, **kwargs)
        snapshot2 = tc.take_snapshot()  # 建立快照
        top_stats = snapshot2.compare_to(snapshot1, 'lineno')  # 比较两段快照之间的内存
        logger.info("--------------------[ Top 10 differences ]----------------------")
        for stat in top_stats[:10]:
            logger.info(stat)
        tc.stop()
        return re_data

    return wrapper
  • 这里其实就是对接口运行前后内存占用进行检测,然后日志打印占用最高内存的十个地方。
  • 接下来我用实际项目接口进行测试


  • 然后使用测试工具测试该接口,我这里使用的yaki,一般来说postman也就足够了。测试后看下docker容器部分日志如下所示:


  • 可以看出占用内存前十的基本都是第三方库,说明该接口我们自己写的代码在性能这块没有出现大的问题。
  • 当然,这只是性能检测的小试牛刀 ,内存占用检测很多实际情况是比较复杂的,但是这已经足以说明装饰器的强大了。
  • 如果你完全没有用过装饰器,那现在开始还不晚,前提是你得知道装饰器的原理,理解什么是自由变量和闭包。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358

推荐阅读更多精彩内容