Python函数进阶-闭包函数和装饰器

目录

前言

一、闭包函数

1.闭函数

2.包函数

3.闭包函数

4.闭包函数实例

实例一:

实例二:

二、装饰器

1.装饰器的功能介绍

2.装饰器的实现方式

(1)首先我们来看以下的源代码,模拟《王者荣耀》的开局场景。

(2)为了进一步优化,要求为该游戏更新一项新需求,使其能够统计该功能的运行时间。

方案一:

方案二:

方案三:

方案四:

方案五:

方案六:

方案七:

优化:

3.闭包函数和装饰器的区别


前言

什么是闭包函数?什么是装饰器?闭包函数和装饰器有什么区别?闭包函数的作用是什么?装饰器的作用是什么?

一、闭包函数

1.闭函数

  • 闭,其实指的就是封闭的意思,就是说这个函数是被封闭起来的。
def f1():
    # 到目前为止这个f1肯定是没有被封闭起来的,因为它现在属于全局
    def f2():
        pass
        # 但是如果在f1里面定义一个f2,这个f2现在就是被封起来了
        # 因为这个f2现在只能在f1的里面局部访问,故此这个f2就叫做《闭函数》
    pass

2.包函数

  • 包函数指的就是这个包函数的内部包含对外层函数作用域名字的引用
def f1():
    x = 10
    
    def f2():
        # 比如要在原有的包函数上内部打印一个 x
        # 然后在外层函数内部定义一个 x=10
        print(x)
        # 这就是在f2的内部引用了外层函数f1的局部作用域的名字,故此叫做《包函数》     
    pass

3.闭包函数

  • 前面我们说这个f2是被封闭在了f1的局部,要调用它的话只能在f1内部调用,全局是没办法访问到f2的。故此给出一个需求:就是要让全局也能够访问这个f2
def f1():
    x = 10

    def f2():
        print(x)
    # 函数是可以传递的,可以当做返回值被另一个函数返回
    # 如此可以在这里直接f2 return 出去
    return f2  # 这里f2一定不要加括号,加括号就是返回f2的返回值了
    # 不加括号返回的才是f2的内存地址


# 现在全局想要访问f2的内存地址就很容易了
# 直接调用f1
res = f1()  # 这个f1返回的就是f2的内存地址了,现在用一个res来接着
print(res)  # 输出结果:.f2 at 0x000002C2EE3754C8>
# f1.local:就是f1局部的f2的内存地址
# 既然res拿到了f2的内存地址,我们便可以用括号来调用它
res()  # 输出结果:10

>>>输出结果:
.f2 at 0x000002140F1854C8>
10

Process finished with exit code 0
  • 现在我们就实现了一个非常厉害的功能,因为我们打破了作用域的限制,把局部的函数内存地址拿到了全局来调用。故此我们接着往下看。

假如:现在我们在 res() 前面加一个 x=20

def f1():
    x = 10

    def f2():
        print(x)
    return f2


res = f1()
x = 20  # 1.现在我们在 res() 前面加一个 x=20
res()   # 2.这个时候的输出结果依旧还是f1内部的10
# 3.原因是名字的查找顺序是在定义阶段就确定好了的,和我们在哪里调用是没有关系的
# 4.不管我们在哪里调用他要找的 x 永远都是它自己包里面的那个 x

>>>输出结果:
10

Process finished with exit code 0

故此我们要想在外部实现应该如下:

def f1(x):   # 2.我直接把这个 x 作为 f1 的参数
    # x = 10  # 1.我们在这里写 x=10 也就是相当于把x写死了,故此将他注释掉重新修改

    def f2():
        print(x)
    return f2


res = f1(30)  # 3.然后外面调用 f1 的时候,给他传一个 10 就可以了
x = 20
res()   # 4.故此后面再调用的时候,这个f2内部用的就是我们传给f1的值了

>>>输出结果:
30

Process finished with exit code 0
  • 上面的闭包代码就是我们闭包函数的整个过程,我们在外部函数 f1 下定义了一个内部函数 f2,并且外部函数的返回值就是内部函数,同时在内部函数中,我们引用到了外部函数的变量 x ,而闭包的作用就是可以将外层函数的变量保存在内存中而不被销毁

(1)什么时候需要用到闭包函数?

  • 当原本的函数(也就是f2)不能再去给它增加新的形参的时候,而你这个函数的内部恰好又需要有外部传参进来,这个时候就可以用到闭包函数了。

4.闭包函数实例

实例一:

需求:创建一个函数add(),每次调用该函数都只能传一个数 num ,每次返回的结果都是基于上一次结果的值 sum 进行累加操作。例如,sum的默认值为0,如果我们依次调用 add(10)、add(20)、add(30) 后,期望得到的最终结果是 sum = 60。

(1)对于该问题,因为需要在函数内部对函数外部的变量进行处理,我们可能会考虑使用 global 来处理。(点我快速了解global)

sum = 0


def get_add_sum(num):
    global sum
    sum += num
    return sum


print(get_add_sum(10))  # 输出:10
print(get_add_sum(20))  # 输出:30
print(get_add_sum(30))  # 输出:60

print(sum)  # 输出:60
  • 上面代码中,我们在函数中通过全局变量 global 将 sum 声明为全局变量,最终返回的结果也符合我们的期望。但因为全局变量太灵活了,不同模块函数都能自由访问到全局变量,所以一般不推荐在函数内部中定义全局变量。

(2)对于上面的问题,除了使用全局变量外,我们还可以通过 闭包 来实现。

sum = 0


def get_add_sum(sum):    # 外层函数
    def add_num(num):    # 内层函数
        nonlocal sum
        sum += num
        return sum
    return add_num


# 闭包的作用:以将外层函数的变量保存在内存中而不被销毁。
# 简单点来说每add一次它就会自己记住这一次的数字,下次读取的时候再调用
add = get_add_sum(sum)
print(add(10))  # 输出:10
print(add(20))  # 输出:30
print(add(30))  # 输出:60

print(sum)  # 输出:0
  • 在上面的闭包函数中,定义了外层函数和内层嵌套函数,执行过程中,调用外层函数 get_add_sum 返回的就是内层嵌套函数 add_num ,因为Python中函数也是对象,所以可以直接返回 add_num 。
  • 而在内层嵌套函数中则实现了我们想要的累加操作,在这里我们需使用关键字 nonlocal来声明外层函数中的变量,否则无法对外层函数中的变量进行修改,其作用域只在闭包函数里面,所以当我们在最后打印 sum 最终结果,输出的仍然是其初始值 0 。(点我了解nonlocal)

实例二:

def outer():
    res = []
    for i in range(3):
        print("外部的i值:{}".format(i))

        def inner(x):
            print("内部的i值:{}".format(i))
            return i + x
        res.append(inner)
    return res


temp = outer()
# 写法一:
# res = [i(10) for i in temp]
# 写法二:
res = []
for i in temp:
    res.append(i(10))   # 此时的 i(10) 可以理解为 inner(10)

print(res)

>>>输出结果:
外部的i值:0
外部的i值:1
外部的i值:2
内部的i值:2
内部的i值:2
内部的i值:2
[12, 12, 12]

Process finished with exit code 0
  • 可以看到 res 的结果并不是 [10, 11, 12] ,因为 i 是外层函数的变量,并且其值是按 0,1,2 变化,因为该函数中无法保存外层函数的变量,故对于内部函数,其实际只能访问到最后外部变量的值 2 ,导致最终结果为:[12, 12, 12]。

(1)在这里,我们可以使用闭包来保存函数的外部变量,修改代码如下:

def outer(i):
    print("外部的i值:{}".format(i))
    def inner(x):
        print("内部的i值:{}".format(i))
        return i + x
    return inner


def demo():
    res = []
    for i in range(3):
        res.append(outer(i))
    return res

temp = demo()
res = [i(10) for i in temp]
print(res)

>>>输出结果:
外部的i值:0
外部的i值:1
外部的i值:2
内部的i值:0
内部的i值:1
内部的i值:2
[10, 11, 12]

Process finished with exit code 0

二、装饰器

1.装饰器的功能介绍

器:器具、工具(编程中我们需要创造一个函数的时候也就是相当于创建一个工具)

装饰:为相关事物添加额外功能

装饰器:在不修改装饰对象的源代码,也不修改调用方式的前提下定义一个函数(或者类),这个函数的功能就是用来装饰其他函数的,也即是说这个函数是用来给其他函数添加额外功能的

(1)为什么需要装饰器?而不是在原代码上修改?

  • 首先给大家普及一下开放封闭原则他是所有面向对象原则的核心,何为该原则?开放:对外拓展功能(增加功能)开放,拓展功能的意思是在源代码上不做任何的改变的情况下,为其增加功能。封闭:对修改源代码是封闭的
  • 我们开发一款程序,这款程序应该是可扩展的而不是可修改的,两者看起来好像是矛盾的,但其实不然,因为我们写完一款软件之后这个软件是要放到线上环境去运行的,就是说你这个软件要正式跟用户接触了。这个时候我们就不能轻易的去对它进行修改,因为当我们去对他进行修改的话就意味着要把这个软件终止运行。例如《王者荣耀》假如官方需要关软件更新,官方会提前告知几月几号、几点到几点,这种就叫停服维护。当用户再次使用的时候就是新软件了。
  • 线上运行的程序不能够随意关闭,但问题是如果我我们经常需要去改线上环境的代码,就会存在风险。因为当我们去改线上代码的时候把软件关掉,改完了之后如果我们能够保证这个代码没有任何问题的,当我再重新上线给用户使用的时候也没有任何bug自然是皆大欢喜。但实际情况是,我们在写程序的时候随着程序越写越多有一些问题是我们在测试的时候是测试不到的。
  • 当一款程序的功能一旦设计完成,能够正常完成赋予它的工作时,如果在对该功能没有更改需求的情况下,只想在此基础上增加额外的功能则没有必要去修改源代码,而是再写一个新的函数来给它增加新的功能。

2.装饰器的实现方式

(1)首先我们来看以下的源代码,模拟《王者荣耀》的开局场景。

import time


def inside(group, s):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print('全军出击')


inside('红', 5)
print('---'*20)
inside('蓝', 3)

>>>输出结果:
欢迎来的王者荣耀
你出生在红方阵容
敌军还有5秒到达战场
全军出击
------------------------------------------------------------
欢迎来的王者荣耀
你出生在蓝方阵容
敌军还有3秒到达战场
全军出击

Process finished with exit code 0
  • 点我了解time模块  

(2)为了进一步优化,要求为该游戏更新一项新需求,使其能够统计该功能的运行时间。

方案一:
import time


def inside(group, s):
    star = time.time()
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print('全军出击')
    end = time.time()
    print(f'用时{int(end-star)}秒')


inside('红', 3)

>>>输出结果:
欢迎来的王者荣耀
你出生在红方阵容
敌军还有3秒到达战场
全军出击
用时3秒

Process finished with exit code 0
  • 问题:没有修改调用方式,但是修改了源代码
方案二:
import time


def inside(group, s):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print('全军出击')


star = time.time()
inside('红', 3)  # 要统计该功能的运行时间,
# 直接在外部调用的代码上下直接统计就可以,避免了修改源代码的隐患。
end = time.time()
print(f'用时{int(end-star)}秒')

>>>输出结果:
欢迎来的王者荣耀
你出生在红方阵容
敌军还有3秒到达战场
全军出击
用时3秒

Process finished with exit code 0
  • 问题:虽然既没有修改源代码和调用方式而且还加上了新功能,但是出现了大量重复代码,代码冗余。例如:该 inside 方法有可能不只是在这一处且只是这一次调用,如果引用该方法,当多个地方多次调用则需要逐个增加同样重复的计算时间的代码上去。在编写一款程序的时候尽量不要出现重复的代码。
方案三:
import time


def inside(group, s):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print('全军出击')


def wrapper_inside():
    star = time.time()
    inside('红', 3)  # 要统计该功能的运行时间,
    # 直接在外部调用的代码上下直接统计就可以,避免了修改源代码的隐患。
    end = time.time()
    print(f'用时{int(end-star)}秒')


wrapper_inside()

>>>输出结果:
欢迎来的王者荣耀
你出生在红方阵容
敌军还有3秒到达战场
全军出击
用时3秒

Process finished with exit code 0
  • 问题:解决了方案二的代码冗余,也没有修改被装饰对象的源代码,同时还为其增加了新功能,但是被装饰对象的调用方式被修改了。与此同时 inside 里面需要传递两个参数,这两个被我们写死了,假如一百个地方都在调用同样的功能,但是不可能一直都是传递同样的参数,所以这两个参数不能够写死,要写成变量的形式。
方案四:
import time


def inside(group, s):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print('全军出击')


# 2.因为wrapper函数里面没有 xy 
def wrapper_inside(x, y):   # 所以我们需要将xy定义wrapper的形参
    star = time.time()
    inside(x, y)  # 1.将这里改写成 x, y
    end = time.time()
    print(f'用时{int(end-star)}秒')


wrapper_inside('蓝色', 3)    # 3.调用wrapper的时候给他传值就可以了

>>>输出结果:
欢迎来的王者荣耀
你出生在蓝色方阵容
敌军还有3秒到达战场
全军出击
用时3秒

Process finished with exit code 0

问题:假设以后inside函数的功能本身就要改变,比如我们要加入一个参数 z,然后print里面改为 print(f'{z}出击'),这样一下来的话 wrapper的形参要改,调用wrapper的时候也要改。

方案五:
import time


def inside(group, s, z):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print(f'{z}出击')


# 1.在形参这里写一个*args和**kwargs
def wrapper_inside(*args, **kwargs):   # 3.但是wrapper接收的参数是给inside用的
    star = time.time()
    inside(*args, **kwargs)  # 4.所以inside也要改写
    end = time.time()
    print(f'用时{int(end-star)}秒')


wrapper_inside('蓝色', 3, '炮车')  # 2.这样子写了之后不管怎么给wrapper传参数它都可以正常接收
# 5.所以当我们给wrapper传参的时候是要遵循其所对应的函数(inside)的形参规则的
# 6.所以再次调用wrapper的时候就参照inside的形参规则就行了,我们这里给它加上一个“炮车”

>>>输出结果:
欢迎来的王者荣耀
你出生在蓝色方阵容
敌军还有3秒到达战场
炮车出击
用时3秒

Process finished with exit code 0

问题:wrapper是用来统计被装饰对象的运行时间的,但是我们在wrapper的内部把装饰对象给写死了,固定是inside了。假如存在另一个函数recharge,刚好也需要统计他的运行时间,那么如果继续沿用该方法的话就需要再写一个类似wrapper的方法,也就又出现没必要的重复代码了。

方案六:
import time


def inside(group, s, z):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print(f'{z}出击')

# 19.把这个func定义成outer的参数 将原来的 def outer(): 改为def outer(func):
def outer(func):
# def outer():  # 10.定义一个外包函数将整体包起来
    # 16.但是我们看outer内部,我们再次吧func写死了 固定了只能是inside,
    # 17.为了以后可以把它装饰给其他函数,我们要把它写活,因此注释掉 func = inside 改写
    # 18.func = inside   # 8.既然wrapper需要一个func,就直接定义一个变量func
    # 9.直接将inside赋值给它
    def wrapper_inside(*args, **kwargs):   # 11.原来该函数是在全局的,
        # 12.可以直接调用,但是现在变成局部了全局访问不到了
        star = time.time()
        func(*args, **kwargs)  # 1.因此需要将该函数改为变量func
        # 2.但是这样就会引发新的问题,因为wrapper函数为了把被装饰对象写活把inside替换成为了func
        # 3.导致需要向wrapper的内部传一个参数func。

        end = time.time()
        print(f'用时{int(end-star)}秒')
    return wrapper_inside  # 因此我们需要将它return出来

# wrapper_inside('蓝色', 3, '炮车')  # 13.因此我们需要引用该方法的时候就需要改写,故此这里注释掉
# wrapper_inside('蓝色', 3, '炮车', inside)  4.但是我们不能在这里直接传
# 5.因为这里的参数都是直接导进func所代表的函数内部的也就是inside内部
# 6.'蓝色', 3等等这些函数都是给inside用的,并不会停留在wrapper
# 7.因此,我们可以运用闭包函数的知识
# res = outer()  # 14.因为outer()返回的是wrapper,所以我们用一个res来接住
# 15.但是现在还有一个问题,就是wrapper需要一个变量func,我们是通过outer包给它的
res = outer(inside)# 20.将原来的res = outer() 改为 res = outer(inside)
# 21.现在这个装饰器就被我们写活了
# 22.然后我们根据inside的形参来运行
res('蓝色', 3, '炮车')

>>>输出结果:
欢迎来的王者荣耀
你出生在蓝色方阵容
敌军还有3秒到达战场
炮车出击
用时3秒

Process finished with exit code 0

问题:还是修改了inside的调用方式,原来是调用inside现在变成了res

方案七:
import time


def inside(group, s, z):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print(f'{z}出击')


def outer(func):
    def wrapper_inside(*args, **kwargs):
        star = time.time()
        func(*args, **kwargs)
        end = time.time()
        print(f'用时{int(end - star)}秒')
    return wrapper_inside


# res = outer(inside)    # 1.既然这个地方可以赋值给res,同样也可以赋值给abcd
# 2.所以,将原来的注释掉,我们把它赋值给inside,
inside = outer(inside)
inside('蓝色', 3, '炮车')   # 3.现在我们单独看这一行,对于原来的作者是不是就不知道发发生了什么
# 4.以为是原来的inside,但实际上早已经被我们偷梁换柱了,不在是当初的inside
# 5.到这里装饰器的基本功能就被我们实现了

# 装饰器:在不修改装饰对象的源代码,也不修改调用方式的前提下定义一个函数(或者类),
# 这个函数的功能就是用来装饰其他函数的,也即是说这个函数是用来给其他函数添加额外功能的

>>>输出结果:
欢迎来的王者荣耀
你出生在蓝色方阵容
敌军还有3秒到达战场
炮车出击
用时3秒

Process finished with exit code 0
优化:

现在我们对该功能再优化一下,假如我们的原功能是有返回值的该怎么办呢?例如:

def inside(group, s, z):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print(f'{z}出击')
    return '王者荣耀正在运行!'

步骤一:

我们先直接print一下看看

import time


def inside(group, s, z):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print(f'{z}出击')
    return '王者荣耀正在运行!'


def outer(func):
    def wrapper_inside(*args, **kwargs):
        star = time.time()
        func(*args, **kwargs)
        end = time.time()
        print(f'用时{int(end - star)}秒')
    return wrapper_inside


inside = outer(inside)
print(inside('蓝色', 3, '炮车'))

>>>输出结果:
欢迎来的王者荣耀
你出生在蓝色方阵容
敌军还有3秒到达战场
炮车出击
用时3秒
None

Process finished with exit code 0

可以看到我们print的结果是None,为什么呢?原因很简单,我们这里的inside接收的是outer(inside)中wrapper的内存地址。然后我们看看wrapper此时是不是没有返回值,是不是没有return?因此我们还需要对它进行修改。

def wrapper_inside(*args, **kwargs):
        star = time.time()
        func(*args, **kwargs)
        end = time.time()
        print(f'用时{int(end - star)}秒')

步骤二:

import time


def inside(group, s, z):
    print('欢迎来的王者荣耀')
    print(f'你出生在{group}方阵容')
    print(f'敌军还有{s}秒到达战场')
    time.sleep(s)
    print(f'{z}出击')
    return '王者荣耀正在运行!'


def outer(func):
    def wrapper_inside(*args, **kwargs):
        star = time.time()
        # func(*args, **kwargs)
        # 4.故此我们用response将func接住,我们把原来的注释掉
        response = func(*args, **kwargs)
        end = time.time()
        print(f'用时{int(end - star)}秒')
        # 1.首先我们需要明白我们需要返回的是原inside中的值,
        # 2.而此时的inside是赋值在func中的,因此此时的func也即是inside的化身
        # 3.所以这里外面返回的时候自然也即是func中的值
        return response     # 5.然后再将response返回
    return wrapper_inside


inside = outer(inside)
print(inside('蓝色', 3, '炮车'))

>>>输出结果:
欢迎来的王者荣耀
你出生在蓝色方阵容
敌军还有3秒到达战场
炮车出击
用时3秒
王者荣耀正在运行!

Process finished with exit code 0

可以看到无论被装饰对象是有具有返回值都不影响整个程序的运行,同时也满足了装饰器所应具备的各种要求。到这里一个完整的装饰器就提现出来了

3.闭包函数和装饰器的区别

在Python中,闭包传递的参数是变量,装饰器传递的参数是函数对象,它们只是在传参内容上有不同。那么装饰器是不是属于闭包的一种呢,我们要怎么判断一个函数是否是闭包呢?

def demo_outer(x):
    """
    闭包
    """
    def demo_inner(y):
        print("x的值:{}, y的值:{}, x + y 的值:{}".format(x, y, x + y))
    return demo_inner


def demo_decorator(func):
    """
    装饰器
    """
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper


def method():
    pass


do = demo_outer(5)  # 闭包
print(f"闭包的属性:{do.__closure__}")

dd = demo_decorator(method)  # 装饰器
print("装饰器的属性:{}".format(dd.__closure__))

>>>输出结果:
闭包的属性:(,)
装饰器的属性:(,)

Process finished with exit code 0)

所以结合上面的结果,我们也可以这样理解:装饰器本质上就是一个闭包函数,它只是一个传递函数对象的闭包函数

声明:以上内容仅供学习交流,如有错误欢迎指正!!!

你可能感兴趣的:(python,开发语言)