闭包函数 & 装饰器

引入

定义函数和定义变量是类似的,变量名绑定的是值的内存地址,而函数名绑定的是代码块的内存地址,函数也可以像变量一样被使用。

函数对象

函数对象的精髓就是可以把函数当做变量使用。函数对象的使用方式如下:

  • 函数名加括号调用

函数名加括号调用会运行函数体代码,可以使用变量接收函数返回值。

def func():
    print('func')
    return 'func'
res = func()
print(res)
  • 可以进行赋值

函数定义完成之后,不加括号表示的函数体代码的内存地址。

def func():
    print('func')
print(func)  # 函数体代码的内存地址
foo = func  # 将foo这个名字也指向func函数体代码的内存地址
foo()  # 相当于func()
  • 可以当做函数的参数
def func(x):
    print(x)
    x()
def foo():
    print('from foo')

func(foo) 
# 将foo函数体代码内存地址当做参数传递给func函数
  • 可以当做函数的返回值
def func(x):
    x()
    return x
def foo():
    print('from foo')
res = func(foo)
print(res)
  • 可以当做容器类型数据的元素
def func():
    print('from func')
def foo():
    print('from foo')
    
func_list = [func, foo]
for function in func_list:
    function() 

函数嵌套

函数嵌套分为两个方面理解:

函数的嵌套定义

在一个函数内定义其他函数。

def func():
    print('from func')
    def foo():
        print('from foo')

函数的嵌套调用

在调用一个函数的过程中又调用其他函数。

# 定义函数:比较两个值的大小
def max2(x,y):
    if x > y:
        return x
    else:
        return y# 定义函数:比较四个值的大小
    
def max4(a,b,c,d):
#     第一步,比较a,b
    res = max(a,b)
#     第二部,比较res和c
    res1 = max(res,c)
#     第三步,比较res1 和d
    res2 = max(res1,d)
    return res2
res = max(2,3,5,1)
print(res)

闭包函数

什么是闭包函数

闭函数指的是在一个函数内部的函数,即嵌套在函数内部的函数。

包函数指的是内部函数对外层函数(非全局作用域)作用域名字的引用。

闭包函数基于函数对象,可以将函数返回到任意位置调用,但是作用域的关系在定义函数的时候就被确定了,与函数的调用位置无关。

如果内嵌函数包含对外部函数作用域中名字的引用,该内嵌函数就是闭包函数。

def func():
    x = 100
    def foo():
        print(x)  # 对外部函数作用域名字的引用
    foo()
    return foo
res = func()

闭包函数作用

闭包函数可以实现另一种为函数传参的方式,另外装饰器也是基于闭包函数的。

# 函数传参方式一:函数体需要的参数直接定义为形参
def func(x, y):
    print(x, y)

# 函数传参方式二:闭包函数,内嵌函数对外层函数名字的引用
def func(x):
    def foo():
        print(x)

装饰器

为啥要用装饰器

软件的设计应该遵循开放封闭原则,即对扩展功能开放,对修改代码封闭。意思就是说当有新的需求或者功能时,可以对现有的代码进行扩展,但是代码一旦设计完成,可以独立完成某一项需求时就不要对其进行修改。

看起来是两个对立面,因为一个程序包含的所有代码有可能改一部分,就会出现蝴蝶效应,一处改处处错,但是又必须为程序提供扩展的方式,这就用到了装饰器。

什么是装饰器

装饰指的是为其他事务添加一些额外的东西进行点缀,而器指的就是工具,可以定义成函数。因此装饰器指的就是定义一个函数,这个函数是用来为其他函数添加额外的功能。

抽象的讲装饰器就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。装饰器经常被用于登录校验、权限校验等场景,有了装饰器就可以抽出大量的与函数本身功能无关的代码然后重用。

无参装饰器实现

装饰器分为无参装饰器和有参装饰器,实现原理相同,基于闭包函数、函数嵌套以及函数对象。

通过一个简单的例子来推导无参装饰器的实现---计算一个函数的执行时间。

首先介绍一下time模块的简单使用,可以用来计算时间。

import time  # 导入模块
start_time = time.time()  # 当前时间
end_time = time.time()  
res = end_time - start_time  # 相减得到时间差

现在就来推导如何实现无参装饰器 --- 计算下述函数的执行时间。

import time

def func(x, y):
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)

解决方案一

import time

def func(x, y):
    start_time = time.time()
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)
    end_time = time.time()
    print(end_time - start_time)  # 函数运行时间
     return x, y
func(1, 2)

该方案虽然解决了计算函数执行时间的需求,但是违背了开放封闭原则,虽然调用方式未发生改变,但是func函数的源代码改变了。

解决方案二

在调用函数前后加上时间,然后相减得到运行时间

import time

def func(x, y):
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)
    return x, y

start_time = time.time()  # 当前时间
func(1, 2)
end_time = time.time()  
print(end_time - start_time)

该方案虽然解决了计算函数执行时间的需求,也没有违背开放封闭的原则,但是如果计算函数运行时间是一个非常常用的功能,那么代码就会出现非常多的重复部分,代码冗余。

解决方案三

将方案二中计算时间的代码封装成一个函数,减少代码冗余。

import time

def func(x, y):
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)
    return x, y
    
def wrapper():
    start_time = time.time()  # 当前时间
    res = func(1, 2)
    end_time = time.time()  
    print(end_time - start_time)
wrapper()

该方案解决了计算函数运行时间的需求,也减少了代码冗余,但是改变了原函数的调用方式,并且func函数的实参被写死了,只能计算func(1, 2)的运行时间。

解决方案四

分析方案三,在调用wrapper函数时会间接调用func函数,因此可以在定义wrapper函数时为其定义和func函数相同的形参,由此实现通过给wrapper函数传参间接为func函数传参,这样func函数的参数就写活了。

import time

def func(x, y):
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)
    return x, y
    
def wrapper(x, y):
    start_time = time.time()  # 当前时间
    res = func(x, y)
    end_time = time.time()  
    print(end_time - start_time)
wrapper(1, 2)

该方案解决了需求,也解决了参数固定的问题,但是调用方式改变了,而且在给wrapper函数定义形参的时候必须按照func形参的格式,如果func形参个数发生变化,那么定义wrapper时形参也必须变化。

解决方案五

如何不让wrapper函数的形参收到func函数形参的影响是本方案需要实现的目的。在定义wrapper函数的时候,将wrapper函数的形参定义为可变长度参数,可以用来接收任何符合语法规则的实参,只要在传实参的时候和func保持一致即可,而定义阶段不管func的形参如何变化,wrapper都可以接收所有形式的实参,不需要改变wrapper形参。

import time

def func(x, y):
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)
    return x, y
    
def wrapper(*args, **kwargs):
    start_time = time.time()  # 当前时间
    res = func(*args, **kwargs)  # 这里是实参
    end_time = time.time()  
    print(end_time - start_time)
wrapper(1, 2)

该方案解决了方案四中wrapper函数形参收到func形参限制的问题,但是函数的调用方式变了,而且wrapper函数只能计算func这一个函数的运行时间。

解决方案六

想要实现计算任意函数的运行时间,首先想到的是可以将func改为变量名,用来接收函数内存地址的一个变量名,然后为此变量名传参,代码如下:

import time

def func(x, y):
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)
    return x, y
    
def wrapper(function, *args, **kwargs):
    start_time = time.time()  # 当前时间
    res = function(*args, **kwargs)  # 这里是实参
    end_time = time.time()  
    print(end_time - start_time)
wrapper(func, 1, 2)

上述方案虽然可以给func传值,但是我们需要的是不改变被装饰对象的调用方式和源代码,这里还是改变了原函数的调用方式,并且wrapper函数的形参与func函数的形参也不一致了。因此,可以考虑使用闭包函数为内嵌函数传参。

import time

def func(x, y):
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)
    return x, y

def outer(function):
    def wrapper(*args, **kwargs):
        start_time = time.time()  # 当前时间
        res = function(*args, **kwargs)  # 这里是实参
        end_time = time.time()  
        print(end_time - start_time)
        return res  # 返回被装饰对象的返回值
    return wrapper

wrapper = outer(func)
wrapper(1, 2)

上述方案由于wrapper函数本身是在全局名称空间的,由于需要给wrapper函数传参数,所以放到了局部名称空间,当给wrapper传参结束后,需要将wrapper函数拿回全局名称空间。

该方案依旧没有解决被装饰对象调用方式的问题。

解决方案七 - 最终版本

outer函数的返回值可以随便赋值给任意的变量,可以赋值给wrapper,那么自然可以赋值给func,如果需要被装饰函数的返回值,在内部函数wrapper中返回被装饰函数的返回值即可。

import time

def func(x, y):
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)
    return x, y

def outer(function):
    def wrapper(*args, **kwargs):
        start_time = time.time()  # 当前时间
        res = function(*args, **kwargs)  # 这里是实参
        end_time = time.time()  
        print(end_time - start_time)
        return res  # 返回被装饰对象的返回值
    return wrapper

func = outer(func)
func(1, 2)

至此就完成了在不修改func的源代码也不修改调用方式的前提下给func增加了新的功能
此时的func指向的内存地址已经不是原来的func指向的内存地址,而是wrapper函数的内存地址,只是将函数名改为了func。

无参装饰器总结

通过以上无参装饰器的推导过程,就可以得到无参装饰器的一个万能模板以及无参装饰器的原理。

def outer(function):  # outer只是一个名字,取什么名字都可以
    def wrapper(*args, **kwargs):
        res = function()
        return res
    return wrapper

被装饰函数名 = outer(被装饰函数名)  # 无参装饰器的原理
被装饰函数名()

语法糖

为了简洁而优雅地使用装饰器,Python提供了专门的装饰器语法来取代被装饰函数名 = outer(被装饰函数名),需要在被装饰对象的正上方单独添加一行@outer,当解释器执行到@outer时就会调用outer函数,且把它正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名。

import time

def outer(function):
    def wrapper(*args, **kwargs):
        start_time = time.time()  # 当前时间
        res = function(*args, **kwargs)  # 这里是实参
        end_time = time.time()  
        print(end_time - start_time)
        return res  # 返回被装饰对象的返回值
    return wrapper

@outer
def func(x, y):
    time.sleep(3)  # 可以让程序休眠3s
    print(x, y)
    return x, y

有参装饰器实现

实现认证装饰器,需要判断用户名密码是来自哪里?比如文件或者数据库;具有认证功能;认证通过后才能执行被装饰对象的代码;语法糖限制,outer函数只能携带一个参数---被装饰函数的内存地址。因此实现代码如下

def auth(db_type):
    def outer(func):  # 由于语法糖限制只能传递一个参数,索引db_type只能从外部函数获取
        def wrapper(*args, **kwargs):
            user = input('name').strip()
            pwd = input('pwd').strip()
            if db_type == 'file':
                print('我是基于文件登录的')
                if user == 'egon' and pwd == '123':
                    res = func(*args, **kwargs)
                    return res
                else:
                    print('登陆失败')
            elif db_type == 'mysql':
                print('我是基于数据库msql登录的')
            else:
                print('我不支持此类型')
        return wrapper
    return outer

@auth('file')  # 将auth('file')函数的返回结果放在@后面,即@outer
def index(x, y):
    print(x, y)

有参装饰器总结

通过上述代码可以总结出有参装饰器的万能模板以及有参装饰器的原理。

def 有参装饰器(x,y,z):
    def outer(func):
        def wrapper(*args, **kwargs):
            res = func(*args, **kwargs)
            return res
        return wrapper
    return outter

@有参装饰器(1,y=2,z=3)  # 原理是执行有参装饰器(x, y, z),将返回值放在@后面,得到无参装饰器@outer,然后就是无参装饰器的原理。
def 被装饰对象():
    pass

wraps

装饰器的基本原则就是偷梁换柱,就是将wrapper函数做的和原函数一模一样才行:不改变源代码不改变调用方式,但是增加了其他的功能,上述总结的装饰器模板,有一点点的小瑕疵。比如:当打印被装饰过的函数的函数名或者被装饰函数有说明文档的时候,如果打印出来就会暴露调用的函数并不是原函数了。

import time

def timer(func):
    def wrapper(*args,**kwargs):
        start_time = time.time()
        print('hello')
        time.sleep(2)
        end = time.time()
        print(end-start_time)
        res = func(*args,**kwargs)


        return res
    return wrapper

@timer
def index(x,y):
    '''
    doc:我是index
    :param x:
    :param y:
    :return:
    '''
    print(x,y)

index(1,2)

print(index.__name__) # 查看函数名,wrapper
print(index.__doc__) # 查看函数说明文档None

如何解决这个问题呢?可以参考以下两种方式:

- 方式一
# 手动将原函数的属性赋值给wrapper函数
# 1、函数wrapper.__name__ = 原函数.__name__
# 2、函数wrapper.__doc__ = 原函数.__doc__
# wrapper.__name__ = func.__name__
# wrapper.__doc__ = func.__doc__
缺点:每个函数的属性有很多,难道真的要一条一条的进行修改吗?


- 方式二:wraps
wraps:本质也是一个装饰器,会将原函数的属性全部赋值给wrapper函数

from functools import wraps
def outer(func):
    # 将被装饰函数当做参数传给wraps
    @wraps(func)
    def wrapper(*args,**kwargs)
        res = func(*args,**kwargs)
        return res
    return wrapper
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容