转:python装饰器详解-程序员宅基地

技术标签: python  

传送门

二话不说上来就模板:

def decorator(func):
    def inner(*args, **kwargs):
    
        add_other_actions()
        
        return func(*args, **kwargs)
    return inner

Python装饰器的本质

Python的装饰器本质上是一个嵌套函数,它接受被装饰的函数(func)作为参数,并返回一个包装过的函数。
这样我们可以在不改变被装饰函数的代码的情况下给被装饰函数或程序添加新的功能
Python的装饰器广泛应用于缓存、权限校验(如django中的@login_required和@permission_required装饰器)、性能测试(比如统计一段程序的运行时间)和插入日志等应用场景。有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码,增加一个函数的重用性。

试想你写了很多程序,一直运行也没啥问题。有一天老板突然让你统计每个程序都运行了多长时间并比较下运行效率。此时如果你去手动修改每个程序的代码一定会让你抓狂,而且还破坏了那些程序的重用性。聪明的程序员是绝不能干这种蠢事的。此时你可以编写一个@time_it的装饰器(代码如下所示)。如果你想打印出某个函数或程序运行时间,只需在函数前面@一下,是不是很帅?

import time

def time_it(func):
    def inner():
        start = time.time()
        func()
        end = time.time()
        print('用时:{}秒'.format(end-start))
    return inner

@time_it
def func1():
    time.sleep(2)
    print("Func1 is running.")

if __name__ == '__main__':
    func1()
运行结果如下:

Func1 is running.

用时:2.0056326389312744

 

嵌套函数

如果在一个函数的内部还定义了另一个函数(注意: 是定义,不是引用!),这个函数就叫嵌套函数。外部的我们叫它外函数,内部的我们叫他内函数

我们先来看一个最简单的嵌套函数的例子。我们在outer函数里又定义了一个inner函数,并调用了它。你注意到了吗? 内函数在自己作用域内查找局部变量失败后,会进一步向上一层作用域里查找。

def outer():
    x = 1
    def inner():
        y = x + 1
        print(y)
    inner()

outer() #输出结果 2

如果我们在外函数里不直接调用内函数,而是通过return inner返回一个内函数的引用 这时会发生什么呢? 你将会得到一个内函数对象,而不是运行结果。

def outer():
    x = 1
    def inner():
        y = x + 1
        print(y)
    return inner

outer() # 输出<function outer.<locals>.inner at 0x039248E8>
f1 = outer()
f1() # 输出2

上述这个案例比较简单,因为outer和inner函数都是没有参数的。我们现在对上述代码做点改动,加入参数。你可以看到外函数的参数或变量可以很容易传递到内函数。

def outer(x):
    a = x

    def inner(y):
        b = y
        print(a+b)

    return inner

f1 = outer(1) # 返回inner函数对象
f1(10) # 相当于inner(10)。输出11

如果上例中外函数的变量x换成被装饰函数对象(func),内函数的变量y换成被装饰函数的参数,我们就可以得到一个通用的装饰器啦(如下所示)。你注意到了吗? 我们在没对func本身做任何修改的情况下,添加了其它功能, 从而实现了对函数的装饰。

def decorator(func):
    def inner(*args, **kwargs):
        add_other_actions()
        return func(*args, **kwargs)
    return inner

请你仔细再读读上面这段代码,我们的decorator返回的仅仅是inner函数吗? 答案是不。它返回的其实是个闭包(Closure)。整个装饰器的工作都依赖于Python的闭包原理

闭包(Closure)

闭包是Python编程一个非常重要的概念。如果一个外函数中定义了一个内函数,且内函数体内引用到了体外的变量,这时外函数通过return返回内函数的引用时,会把定义时涉及到的外部引用变量内函数打包成一个==整体(闭包)==返回。我们在看下之间案例。我们的outer方法返回的只是内函数对象吗? 错。我们的outer函数返回的实际上是一个由inner函数和外部引用变量(a)组成的闭包!

def outer(x):
    a = x

    def inner(y):
        b = y
        print(a+b)

    return inner


f1 = outer(1) # 返回inner函数对象+局部变量1(闭包)
f1(10) # 相当于inner(10)。输出11

一般一个函数运行结束的时候,临时变量会被销毁。但是闭包是一个特别的情况。当外函数发现,自己的临时变量会在将来的内函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量同内函数绑定在一起。这样即使外函数已经结束了,内函数仍然能够使用外函数的临时变量。这就是闭包的强大之处。

如何编写一个通用的装饰器

我们现在可以开始动手写个名为hint的装饰器了,其作用是在某个函数运行前给我们提示。这里外函数以hint命名,内函数以常用的wrapper(包裹函数)命名。

def hint(func):
    def wrapper(*args, **kwargs):
        print('{} is running'.format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@hint
def hello():
    print("Hello!")

我们现在对hello已经进行了装饰,当我们调用hello()时,我们可以看到如下结果。

 hello()
hello is running.
Hello!

值得一提的是被装饰器装饰过的函数看上去名字没变,其实已经变了。当你运行hello()后,你会发现它的名字已经悄悄变成了wrapper,这显然不是我们想要的(如下图所示)。这一点也不奇怪,因为外函数返回的是由wrapper函数和其外部引用变量组成的闭包。

hello.__name__
'wrapper'

为了解决这个问题保证装饰过的函数__name__属性不变,我们可以使用functools模块里的wraps方法,先对func变量进行wraps。下面这段代码可以作为编写一个通用装饰器的示范代码,注意收藏哦。

from functools import wraps

def hint(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('{} is running'.format(func.__name__))
        return func(*args, **kwargs)
    return wrapper


@hint
def hello():
    print("Hello!")

恭喜你,你已经学会写一个比较通用的装饰器啦,并保证装饰过的函数__name__属性不变啦。当然使用嵌套函数也有缺点,比如不直观。这时你可以借助Python的decorator模块(需事先安装)可以简化装饰器的编写和使用。如下所示。

from decorator import decorator

@decorator
def hint(func, *args, **kwargs):
    print('{} is running'.format(func.__name__))
    return func(*args, **kwargs)

编写带参数的高级装饰器

前面几个装饰器一般是内外两层嵌套函数。如果我们需要编写的装饰器本身是带参数的,我们需要编写三层的嵌套函数,其中最外一层用来传递装饰器的参数。现在我们要对@hint装饰器做点改进,使其能通过@hint(coder=“John”)传递参数。该装饰器在函数运行前给出提示的时候还显示函数编写人员的名字。完整代码如下所示:

from functools import wraps


def hint(coder):
    def wrapper(func):
        @wraps(func)
        def inner_wrapper(*args, **kwargs):
            print('{} is running'.format(func.__name__))
            print('Coder: {}'.format(coder))
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper


@hint(coder="John")
def hello():
    print("Hello!")

下面这段代码是一段经典的Python装饰器代码,显示了@cache这个装饰器怎么编写和工作的。它需要使用缓存实例做为一个参数,所以也是三层嵌套函数。

import time
from functools import wraps


# 装饰器增加缓存功能
def cache(instance):
    def wrapper(func):
        @wraps(func)
        def inner_wrapper(*args, **kwargs):
            # 构建key: key => func_name::args::kwargs
            joint_args = ','.join((str(x) for x in args))
            joint_kwargs = ','.join('{}={}'.format(k, v) for k, v in sorted(kwargs.items()))
            key = '{}::{}::{}'.format(func.__name__,joint_args, joint_kwargs)
            # 根据key获取结果。如果key已存在直接返回结果,不用重复计算。
         result = instance.get(key)
            if result is not None:
                return result
            # 如果结果不存在,重新计算,缓存。
         result = func(*args, **kwargs)
            instance.set(key, result)
            return result
        return inner_wrapper
    return wrapper


# 创建字典构造函数,用户缓存K/V键值对
class DictCache:
    def __init__(self):
        self.cache = dict()

    def get(self, key):
        return self.cache.get(key)

    def set(self, key, value):
        self.cache[key] = value

    def __str__(self):
        return str(self.cache)

    def __repr__(self):
        return repr(self.cache)


# 创建缓存对象
cache_instance = DictCache()

Python语法糖调用装饰器

@cache(cache_instance)
def long_time_func(x):
    time.sleep(x)
    return x

# 调用装饰过函数
long_time_func(3)

基于类实现的装饰器

Python的装饰器不仅可以用嵌套函数来编写,还可以使用类来编写。其调用__init__方法创建实例,传递参数,并调用__call__方法实现对被装饰函数功能的添加。

from functools import wraps
#类的装饰器写法, 不带参数
class Hint(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('{} is running'.format(self.func.__name__))
        return self.func(*args, **kwargs)
#类的装饰器写法, 带参数

class Hint(object):
    def __init__(self, coder=None):
        self.coder = coder

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('{} is running'.format(func.__name__))
            print('Coder: {}'.format(self.coder))
            return func(*args, **kwargs)     # 正式调用主要处理函数
        return wrapper
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43702920/article/details/107803152

智能推荐

VBS基本语法_vbs脚本语法-程序员宅基地

文章浏览阅读830次,点赞2次,收藏16次。 一、初识VBSVbs 是一种变量无关、解释性执行的脚本语言。vbs语言中不区分大小写。语句以换行结束。dim 声明变量;批量名称声明,多个变量之间用逗号分隔:set &..._vbs脚本语法

Vuex笔记(三)--- actions和mutations交互时候,传递的是一个包含数据的对象-程序员宅基地

文章浏览阅读565次。actions和mutations交互时候,传递的是一个包含数据的对象。这个对象包含不同数据。eg.state.js: 保存数据const state = { foods: [], shops: []}export default state问题就是mutations.js里面:1.第二个参数foods是用{}包住,foods传递的是一个包含数据的对象。2.第二个...

pytorch中的batch_norm, instance_norm, layer_norm, group_norm_f.batch_norm-程序员宅基地

文章浏览阅读1.3k次。1 BatchNorm1.1 实现Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shifthttp://d2l.ai/chapter_convolutional-modern/batch-norm.html?highlight=batchnorm2d import torch import torch.nn.functi..._f.batch_norm

vue3+ts+element-plus el-table组件二次封装(2024-03-20 TTable组件新增新增第一列既显示(复选、单选)和序列号)_vue3+ts+element plus封装表格-程序员宅基地

文章浏览阅读6.6k次,点赞2次,收藏20次。vue3+ts+element-plus el-table组件二次封装(新增动态设置列的显示与隐藏并且可以随意拖地排序功能、改用vitepress编辑组件文档;新增了单元格编辑功能))新增了单元格编辑键盘事件及第三方组件事件功能;2023-06-08 TTable组件新增整行拖拽排序功能;TTable组件修复表头合并插槽使用失效;TTable组件单元格编辑新增表单校验功能/表格内操作按钮权限配置)新增展开行功能;2024-03-20 TTable组件新增新增第一列既显示(复选、单选)和序列号_vue3+ts+element plus封装表格

如何系统的学习Java达到工作要求?_java自学怎样才能达到企业要求-程序员宅基地

文章浏览阅读270次。1.制定好一下系统的学习规划,每天定量,学完什么知识点就掌握,能自己应用,而不是能看懂,写不出来东西。因为现在有很多人都是这样,表面上我是学会了,但是以上手去写但是怎么也写不出来。2.不要自己一个人闷头学,找一套质量不错的视频教程跟着去学习,听课的过程当中一定要把笔记记录下来。笔记当中只记忆重点的结论。把不懂的地方记下来,有时间就去解决。要学会在学习中记好笔记,一定要记得去复习。3.机会都是留给有准备的人,坚持每天学习也考验你的耐性和毅力,中途如果没有什么事情千万不能松懈,一天都不可以,保持一周6天的学_java自学怎样才能达到企业要求

JS中的async/await的用法和理解_js await-程序员宅基地

文章浏览阅读2.2w次。1、首先需要理解async 和 await的基本含义 async 是一个修饰符,async 定义的函数会默认的返回一个Promise对象resolve的值,因此对async函数可以直接进行then操作,返回的值即为then方法的传入函数// 0. async基础用法测试async function fun0() { console.log(1) return 1}fun0().then( x => { console.log(x) }) // 输出结果 1,.._js await

随便推点

STM32F10X新建工程步骤_菲斯塔固件-程序员宅基地

文章浏览阅读473次,点赞2次,收藏5次。STM32创建新工程第一步下载固件包第二步创建工程第三步完成配置,修改main文件为空执行编译第一步下载固件包网址如下:1、Openedv:http://openedv.com/posts/list/6054.htm2、ST官网:https://www.stmcu.com.cn/Designresource/list/STM32F1/firmware_software/firmware_software固件库介绍:Libraries:CMSIS、STM32F10x_StdPeriph_Driv_菲斯塔固件

Ubuntu虚拟机连接开发板网络设置_ubuntu18 虚拟机连接开发板-程序员宅基地

文章浏览阅读1.4k次。1.VMware必须设置成桥接模式,且要在“编辑-》虚拟网络编辑器”里指定桥接到HOST主机的物理网卡 2.Ubuntu虚拟机、HOST主机和目标开发板的IP必须在同一个网段 3.将网线连接开发板并上电,否则Ubuntu虚拟机的ifconfig不会显示已配置的网络设置..._ubuntu18 虚拟机连接开发板

html5开发之ios屏幕适配,iOS开发屏幕尺寸以及屏幕适配等问题(转载内容)-程序员宅基地

文章浏览阅读377次。原帖地址:http://blog.csdn.net/phunxm/article/details/42174937/仅供我个人收藏学习,原博主如不同意请联系qq651263878进行删除,在此表示感谢以及歉意。1.iPhone尺寸规格后续上市的iPhone7以及iPhone7plus 与六代相同1 inch = 2.54cm = 25.4mm上表中的宽高(width/height)为手机的物理尺..._ios html注入屏幕尺寸适配

计算机制作贺卡教案,教案与学生成果:制作电子贺卡-程序员宅基地

文章浏览阅读719次。教案与学生成果:制作电子贺卡狮山镇松岗中心小学 朱蔼玲【教学目的与要求】(1)、通过利用计算机制作贺卡,让学生理解、掌握并熟练运用word这一章的各知识点。(2)通过练习掌握word提供的强大的编辑功能,并能进行综合运用,完成贺卡的制作。(3)进一步熟练掌握用绘图工具绘制图形、文本框及自选图形的制作与修饰、艺术字的插入等操作。(4)通过制作电子贺卡,培养学生动手实践和主动探究的兴趣。为学生创造现代..._计算机作业电子贺卡

html-1 圆角边框的常用写法_html圆角矩形边框-程序员宅基地

文章浏览阅读2.3k次,点赞3次,收藏15次。<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <tit_html圆角矩形边框

mysql 有链接不上去_求助:MySQL 出现间歇性链接不上~~-程序员宅基地

文章浏览阅读568次。程序平时运行正常,每天总能出现几次,数据库链接不上异常,ORM 使用的是 iBatis ,那位兄弟遇到过呀?未经处理的异常: IBatisNet.DataMapper.Exceptions.DataMapperException: Unable to open connection to "ADO.Net driver for MySQL". ---> System.Security.Au..._为验证提供的消息或签名已被更改

推荐文章

热门文章

相关标签