Python 进阶:函数装饰器

2023-10-27

一、前言

本小节主要梳理函数装饰的用法,循序渐进,逐层增加条件,加大复杂度和难度。

环境说明:Python 3.6、windows11 64位

二、函数装饰器

装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。它不修改原来的函数,还给函数增加新的功能,而是使得调用原函数的时候附加一些功能。

【简单应用】

装饰器一般是一个闭包函数,只是函数接收的参数是一个函数。如下代码,My_Decorator()就是一个装饰器,直接调用即可。
以下装饰器是一个比较简单的应用,装饰器 My_Decorator() 给被装饰函数Introduction()加上了一个新的功能【print('print My_Decorator.inner')】,同时又不影响Introduction()原有功能。

# 定义一个装饰器
def My_Decorator(func): # 传入被装饰的函数
    def inner():
        print('print My_Decorator.inner') 
        func()          # 调用func() 函数,保证不修改被修饰函数,且正常执行
    return inner        # 返回inner 函数

def Introduction():
    name = 'Xindata'
    print('My name is %s!'%name) # 在装饰器函数中调用的时候才执行

# 调用装饰器函数
aa = My_Decorator(Introduction)  # 将inner 赋值变量给aa
print(type(aa)) # 结果为:<class 'function'>,此时aa 是一个函数变量
print(aa)       # 结果为:<function My_Decorator.<locals>.inner at 0x000001F6DBBAB790>
aa()            # 调用aa() 相当于调用inner() 函数,执行结果有2个,一个是【print My_Decorator.inner】是print('print My_Decorator.inner')的结果;一个是【My name is XIndata!】,调用func(),即调用Introduction() 函数的结果。

【带返回值】

上文的被装饰函数Introduction()没有返回值,所以可以通过上文代码实现,但是如果有返回值的怎么办呢,方法也很简单,在inner()函数中调用func()前加上return即可获取到被修饰函数的返回值,具体如下:

# 定义一个装饰器
def My_Decorator(func): 
    def inner():
        print('print My_Decorator.inner') 
        return func()   # 加上return,将调用func() 函数得到的结果进行返回
    return inner        

def Introduction():
    name = 'Xindata'
    print('My name is %s!'%name) 
    return name         # 将名字返回

aa = My_Decorator(Introduction)  
print(aa)       # 结果为:<function My_Decorator.<locals>.inner at 0x000001F6DAEA33A0>
my_name = aa()  # 调用aa() 时,同样会打印两个结果:【print My_Decorator.inner】和【My name is XIndata!】
                # 同时,调用func()(即Introduction())返回的变量name会作为返回值赋值给my_name
print(my_name)  # 结果为:Xindata

【带参数】

以上代码是被装饰函数Introduction()没有参数的情况下适配的情况,当Introduction()有参数时怎么办呢?下面来看看这个升级版本。
这个代码的参数值'Xindata'的传递路径比较长,先是通过inner()name参数,然后传递到Introduction()name参数,然后通过Introduction()return 进行返回,再由inner()return 进行返回,最后再赋值给my_name
image.png

# 定义一个装饰器
def My_Decorator(func): 
    def inner(name):
        print('print My_Decorator.inner') 
        return func(name)   # 加上return,将调用func() 函数得到的结果进行返回
    return inner        

def Introduction(name):
#     name = 'Xindata'
    print('My name is %s!'%name) 
    return name

aa = My_Decorator(Introduction)
print(aa)       # 结果为:<function My_Decorator.<locals>.inner at 0x000001F6DB00FDC0>
my_name = aa('Xindata') # 调用aa() 即(inner())时,给name参数传递参数值'Xindata'。调用aa() 函数打印的两个结果同上一个代码。
                        # 同时,name的参数值'Xindata'值传递给func()(即Introduction())的参数name,并通过Introduction()的return 返回,然后再由inner() 的return 返回,再赋值给my_name
print(my_name)

【兼容各种参数】

以上代码,支持了单一参数的传递,但是作为装饰器,由于其特性,经常会给不同的被修饰函数进行装饰,添加新的功能,而目前的这个装饰器,仅支持单一参数的情况,不能支持多参数,甚至是不同类型的参数,如关键字参数等,为此,需要进一步升级,只需要做一个简单的修改即可,下面看看新的装饰器版本。

# 定义一个装饰器
def My_Decorator(func): 
    def inner(*args,**kw):      # 将name参数改为*args,**kw,便可支持各种类型的参数
        print('print My_Decorator.inner') 
        return func(*args,**kw) # 将name参数改为*args,**kw,便可支持各种类型的参数
    return inner        

def Introduction(name):
#     name = 'Xindata'
    print('My name is %s!'%name) 
    return name

aa = My_Decorator(Introduction)
print(aa)
my_name = aa('Xindata')
print(my_name)

# 如果不需要一些过程,也可以进行链式调用,一步到位
my_name = My_Decorator(Introduction)('Xindata')
print(my_name)

@语法糖】

其实上面讲的都是装饰器比较早期的调用方式,后来官方支持了一个更加时髦的写法:@语法糖。把Python之禅的优雅简洁表现得淋漓尽致。下面一起来看看。

# 定义一个装饰器
def My_Decorator(func): 
    def inner(*args,**kw):
        print('print My_Decorator.inner') 
        return func(*args,**kw)
    return inner        

# 在Introduction()函数定义前,用@My_Decorator 声明用装饰器My_Decorator()装饰Introduction()函数
@My_Decorator
def Introduction(name):
#     name = 'Xindata'
    print('My name is %s!'%name) 
    return name

my_name = Introduction('Xindata') # 调用方式也发生了一些变化,直接调用被修饰的函数即可
print(my_name)

【装饰器带参】

如果需要对不同的场景使用不同的 Introduction(),可以对装饰器再加一层函数,然后在@声明时传递参数,如果是早期的调用方式,则可采用三层链式调用。具体代码如下。

# 定义一个装饰器
def Occasion(params):
    def My_Decorator(func): 
        def inner(*args,**kw):
            print('【%s】print My_Decorator.inner'% params) 
            return func(*args,**kw)
        return inner        
    return My_Decorator

@Occasion('Daily')       # 传递参数给params
def Introduction(name):
    print('My name is %s!'%name) 
    return name

my_name = Introduction('Xindata')
print(my_name)

# 早期调用方式,如下
# 定义一个装饰器
def Occasion(params):
    def My_Decorator(func): 
        def inner(*args,**kw):
            print('【%s】print My_Decorator.inner'% params) 
            return func(*args,**kw)
        return inner        
    return My_Decorator

def Introduction(name):
    print('My name is %s!'%name) 
    return name

my_name = Occasion('Daily')(Introduction)('Xindata') # 三层调用
print(my_name)

# 两串代码结果都是
# 【daily】print My_Decorator.inner
# My name is Xindata!
# Xindata

【多个装饰器】

单个装饰器,基本讲完了,那多个装饰器呢?
假设有d1d1两个装饰器,同时装饰f1函数,早期的方法则是val = d1(d2(f));使用@则是把@d1@d2两个装饰器按顺序应用到f函数上。抽象代码如下:

@d1
@d2
def f():
	print('f')
val = f()
    
# 等同于
def f():
	print('f')
val = d1(d2(f))

那上面的例子,再加一个装饰看看效果:

# 定义一个装饰器
def Occasion(params):
    def My_Decorator(func): 
        def inner(*args,**kw):
            print('【%s】print My_Decorator.inner'% params) 
            return func(*args,**kw)
        return inner        
    return My_Decorator

def Current_Decorator(func): 
    def inner(*args,**kw):
        print('print Current_Decorator.inner') 
        return func(*args,**kw)
    return inner        

@Occasion('Daily')         
@Current_Decorator         # 新增装饰器
def Introduction(name):
    print('My name is %s!'%name) 
    return name

my_name = Introduction('Xindata')
print(my_name)


## 早期调用方式
# 定义一个装饰器
def Occasion(params):
    def My_Decorator(func): 
        def inner(*args,**kw):
            print('【%s】print My_Decorator.inner'% params) 
            return func(*args,**kw)
        return inner        
    return My_Decorator

def Current_Decorator(func): 
    def inner(*args,**kw):
        print('print Current_Decorator.inner') 
        return func(*args,**kw)
    return inner   

def Introduction(name):
    print('My name is %s!'%name) 
    return name

my_name = Occasion('Daily')(Current_Decorator(Introduction))('Xindata') # 加一层调用
print(my_name)

# 两串代码结果都是
# 【Daily】print My_Decorator.inner
# print Current_Decorator.inner
# My name is Xindata!
# Xindata

看完上面这两个代码,你可能犯迷糊了,看打印结果前两行是先执行Occasion('Daily')装饰器,再执行Current_Decorator装饰器。但是看早期调用方式,又不太像,因为Occasion('Daily')执行完这一层,就跑到 Current_Decorator(Introduction)这一层,最后再传递 'Xindata'调用Introduction()。没错!事实就是第一个装饰器执行一半就跑到第二个装饰器执行,如果有第三个装饰器,还会跑到第三个装饰器,执行完第三个装饰器之后,再执行第二个装饰器,再执行第一个装饰器,从外到里再到外,这有点像递归的思想。
为了更好看这个效果,下面抽象一个代码在看看这个效果:

def Activity(func):
    def wrapper1(*args,**kw):
        print('活动开始了!  内层函数第1个执行,func参数值:%s'% func.__name__)
        func(*args,**kw)
        print('活动结束了!')
    print('外层函数第3个执行:Activity,返回wrapper1,开始执行内层函数,从wrapper1开始')
    return wrapper1

def Timing(func): 
    def wrapper2(*args,**kw):
        print('介绍计时开始!内层函数第2个执行,func参数值:%s'% func.__name__)
        func(*args,**kw)
        print('介绍计时结束!')
    print('外层函数第2个执行:Timing,返回wrapper2 给 @Activity')
    return wrapper2     

def Speech(func):
    def wrapper3(*args,**kw):
        print('开始演讲。    内层函数第3个执行,func参数值:%s'% func.__name__)
        func(*args,**kw)
        print('结束演讲。')
    print('外层函数第1个执行:Speech,返回wrapper3 给 @Timing')
    return wrapper3

@Activity
@Timing
@Speech
def Introduction(name):
    print('My name is %s!'%name) 

Introduction('Xindata')

# 运行结果为:
# 外层函数第1个执行:Speech,返回wrapper3 给 @Timing
# 外层函数第2个执行:Timing,返回wrapper2 给 @Activity
# 外层函数第3个执行:Activity,返回wrapper1,开始执行内层函数,从wrapper1开始
# 活动开始了!  内层函数第1个执行,func参数值:wrapper2
# 介绍计时开始!内层函数第2个执行,func参数值:wrapper3
# 开始演讲。    内层函数第3个执行,func参数值:Introduction
# My name is Xindata!
# 结束演讲。
# 介绍计时结束!
# 活动结束了!

执行顺序如下:
image.png
外层函数起装饰效果,内层函数执行新功能。函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。也就是说,外层函数即使不调用函数**Introduction()**也会打印出来。(可以注释掉Introduction('Xindata')运行看看效果。)
所以越靠近被装饰函数的装饰器先装饰后执行,而离得远的相反。可以记为就近原则装饰,执行则相反(类似递归思想)

二、变量作用域

【加上变量】

装饰函数一般都是一个闭包函数,这里单独拿闭包函数来做说明,重新找一个比较简单的例子辅助理解。
当在闭包函数的外层函数定义一个变量时,内层函数可以直接调用。

def func():
    x = 0
    def inner():
        # 调用x
        return x
    return inner

f = func()
print(f()) # 结果为0

如果在内层函数要对外层的变量进行修改,这时便会报错,这个和普通函数调用全局变量又要修改它的场景差不多,只不过在那个场景下,可以通过在函数内部对变量使用global声明为全局变量解决该问题。
那闭包函数是不是也有相关的关键字来声明将x作为闭包函数的“全局变量”呢?还真有,它就是nonlocal
image.png
使用 nonlocal x声明x不是当前内层函数的局部变量。
注意:这里的nonlocal x并不是声明x为全局变量。

def func():
    x = 0
    def inner():
        nonlocal x
        x += 1
        return x
    return inner

f = func()
print(f()) # 结果为1

【多次调用】

当多次调用f()的时候,我们会发现结果是递增的,但是打印x时则报错变量未定义。那么x只是闭包函数内的局部变量,并不是全局变量,但是它是怎么把值进行传递的呢?因为这个x是一个特殊的变量,有一个技术术语,叫自由变量(free variable),指未在本地作用域中绑定的变量。闭包函数会保留自由变量的绑定,使得调用函数时可以继续使用只有变量。

def func():
    x = 0
    def inner():
        nonlocal x
        x += 1
        return x
    return inner

f = func()
print(f()) # 结果为1
print(f()) # 结果为2
print(f()) # 结果为3
print(x)   # 报错:NameError: name 'x' is not defined

可以通过一些属性查看相关的变量和值
f.__closure__中的各个元素对应于f.__code__.co_freevars中的一个名称。这些元素是 cell 对象,有个 cell_contents 属性,保存着真正的值。

f.__code__.co_varnames  # 查看当前函数的局部变量
f.__code__.co_freevars  # 查看当前函数的自由变量
f.__closure__[0].cell_contents # 查看自由变量的值


- End -

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Python 进阶:函数装饰器 的相关文章

随机推荐

  • echarts仪表盘颜色渐变

    echarts仪表盘背景颜色渐变 echarts仪表盘背景颜色渐变 offset设置偏移量 代码 option tooltip formatter a br b c toolbox feature restore saveAsImage s
  • ASP.NET MVC Controller与Areas下面的Controller同名的解决办法

    问题重现 当项目下 Controller HomeController cs时 人在创建一个域Test 之后在建一个同名的HomeController Areas Test Controller HomeController cs 运行报错
  • 软件测试面试题1

    1 问 软件测试的原则 答 软件测试的八个原则 山鬼谣弋痕夕的博客 CSDN博客 软件测试的八个原则 所有测试的标准都是建立在用户需求之上 始终保持 质量第一 的觉悟 当时间和质量冲突时 时间要服从质量 需求阶段应定义清楚产品的质量标准 软
  • Homebrew命令

    安装软件 brew install appname 卸载软件 brew uninstall appname brew remove appname 查看以安装软件 brew list 查看软件相关信息 brew info appname 查
  • 数据挖掘——基于sklearn包的分类算法小结

    目录 一 分类算法简介 二 KNN算法 三 贝叶斯分类算法 四 决策树算法 五 随机森林算法 六 SVM算法 一 分类算法简介 1 概念 1 1 监督学习 Supervised Learning 从给定标注 训练集有给出明确的因变量Y 的训
  • 一次「找回」TraceId的问题分析与过程思考

    用好中间件是每一个开发人员的基本功 一个专业的开发人员 追求的不仅是中间件的日常使用 还要探究这背后的设计初衷和底层逻辑 进而保证我们的系统运行更加稳定 让开发工作更加高效 结合这一主题 本文从一次线上告警问题出发 通过第一时间定位问题的根
  • 信息学奥赛一本通 1179:奖学金

    题目链接 http ybt ssoier cn 8088 problem show php pid 1179 include
  • mysql存储区块链_区块链数据是存在链上还是数据库里?

    在回答这个问题之前 首先要理清 区块链数据 和 链上数据 的概念 区块链数据 区块链数据 广义上包括区块链的区块数据和区块链的状态数据 区块数据记录了区块链上发生的每一笔交易 譬如小明给小王转账了50元 小王充值了20元等类似这样的交易数据
  • A+B PLUS

    大整数加法 思路 把每一位存在数组里 相加 遇10进1 include
  • 基本原理图的制作

    以一个案例演示 完成以下要求 1 采用网络标号进行元件间的连线 2 单独修改元件的封装 标称值等参数 3 采用自动编号的方法对原理图中所有元件进行整体编号 4 修改原理图中相同元器件的封装值 5 完成附图所示原理图的制作 步骤 1 创建文件
  • 『学Vue2+Vue3』Vuex 是什么?vuex 的使用

    一 Vuex 概述 目标 明确Vuex是什么 应用场景以及优势 1 是什么 Vuex 是一个 Vue 的 状态管理工具 状态就是数据 大白话 Vuex 是一个插件 可以帮我们管理 Vue 通用的数据 多组件共享的数据 例如 购物车数据 个人
  • cmake安装与使用

    目录 1 下载与安装 2 Cmake使用 2 1 在window 开始 中点击cmake gui exe 打开cmake程序面板 2 2打开需要编译的cmake代码工程 环境 Windows10 64bit 1 下载与安装 下载地址 htt
  • Web应用程序项目以配置使用IIS。未找到Web服务器

    针对这个问题 本人也从网上找了一下解决办法 但是不是太全面 接下来我会总结一下我所用到过的方法 1 在文件夹下面编辑该Web项目的csproj文件 把UserIIS改为False 2 可以在IIS服务器里面配置一个IISUrl里面的地址 地
  • find、grep--根据内容找文件

    1 可以找到相关的文件名或目录名所在的位置 find name file or dir name linux下的find文件查找命令与grep文件内容查找命令 云社区 华为云 2 找出文本文件的位置 并找出内容包含 关键字 的文件 find
  • 解决vue安装less-loader依赖失败的问题

    vue可视化面板中提供的less loader依赖安装失败 倒是以下代码识别不了 出现错误信息 还有一种情况就是在vue cli视图中安装的less loade版本过高 10 1 0 在我们运行项目时 虽然已经安装了 但是版本过高 出现了不
  • typescript环境安装及IDEA配置typescript

    一 typescript环境安装 1 安装node npm 下载官网安装包 http nodejs cn download 双击运行 2 安装完node npm后 查看是否安装成功 node v npm v 3 安装typescript n
  • es--module模块

    一 初识Module 模块 一个一个的局部作用域的代码块 模块系统需要解决的主要问题 模块化的问题 消除全局变量 管理加载顺序 Module的基本用法 import export 只要你会用到 import 导入 或 export 导出 在
  • SpringBoot基础(1)

    目录 SpringBoot基础 1 SpringBoot基础 2 SpringBoot基础 3 1 hello world 相当简单 pom xml文件中配置
  • c语言写60秒关机小程序,输入我是猪才可关闭:整蛊你的朋友吧

    若想要让朋友不知情的情况下上当 可以在vs环境下 选择左上角把Debug版本改为Release版本运行 然后在我的电脑中此文件夹下点开release文件中的exe程序发给朋友 别轻易改数据 关机程序小游戏 goto语句运用 1 电脑运行起来
  • Python 进阶:函数装饰器

    一 前言 本小节主要梳理函数装饰的用法 循序渐进 逐层增加条件 加大复杂度和难度 环境说明 Python 3 6 windows11 64位 二 函数装饰器 装饰器的典型行为 把被装饰的函数替换成新函数 二者接受相同的参数 而且 通常 返回