18.高级函数(闭包与装饰器)

一、闭包函数

1.闭包基础

前面我们已经学习了函数嵌套,我们再来看一个关于函数嵌套的例子。

外层函数outer_1定义了1个空列表lst,然后调用内层函数inner_1,每次调用时往内层函数传入参数1,inner_1在执行时,lst中添加一个1,然后通过外层函数将lst返回给调用函数。

def outer_1():
    lst = []
    def inner_1(num1):
        lst.append(num1)
    inner_1(1)
    return lst
f1 = outer_1()
f2 = outer_1()
print(f1)
print(f2)

# 输出:
[1]
[1]

我们发现,f1的值为[1],f2的值也为[1],lst的值每次运行都会初始化为空列表,有没有办法让lst可以持续保存存入其中的元素呢?

局部变量在函数执行完后,会在内存中自动释放掉,如果希望局部变量可以长期存在,我们就可以使用闭包函数。

接下来看看闭包函数的结构。

def outer_1():                 # 动作2 进入outer_1执行过程
    lst = []                   # 动作3 生成一个空列表 
    print("I am outer_1")
    def inner_1(num1):         # 动作4 定义一个名为inner_1的内层函数 # 动作8 将参数1传给num1 # 动作13 将参数1传给num1
        lst.append(num1)       # 动作9 向lst添加元素1   # 动作14 向lst再添加元素1
        return lst             # 动作10 将lst返回       # 动作15 将lst返回
    return inner_1             # 动作5 将函数inner_1返回,此时f1获得inner_1函数
f1 = outer_1()                 # 动作1 调用函数outer_1  # 动作6 将返回的结果赋值给f1
print(f1(1))                   # 动作7 调用函数f1(1)    # 动作11 将返回的lst打印出来
print(f1(1))                   # 动作12 再次函数f1(1)   # 动作16 将返回的lst打印出来

# 输出
I am outer_1
[1]
[1, 1]

print(type(f1)) # 输出 <class 'function'>
print(f1) # 输出 <function outer_1.<locals>.inner_1 at 0x109aeb550>

外层函数定义1个空列表lst,内层函数引用外层函数的lst变量,使用append添加元素,并将lst返回,外层函数的最后一行是将内层函数名作为一个对象返回。

我们来仔细看下程序是怎样执行的:

动作1: f1=outer_1(),先执行等号右边的函数

动作2:进入outer_1执行过程

动作3:生产一个名为lst的空列表

动作4:定义一个名为inner_1的内层函数

动作5:将函数inner_1返回

动作6:将返回的结果赋值给f1,此时f1获得了inner_1这个内层函数

动作7:调用函数f1(1),传入一个参数给inner_1()

动作8:直接进入内部函数,形参num接收到数值1

动作9:向lst添加元素1

动作10:返回lst

动作11:打印返回的lst

我们发现外层函数只有在f1=outer_1()的赋值过程中才执行,之后f1(1)只是对内层函数inner_1的调用。第二次执行f1(1)的过程基本相似,只是在第二次执行时,lst并没有从内存中删除,依然保留着第一次执行时的结果,所以在第二次执行后,lst的值是[1,1]。

理解这个过程如果感觉有点晕,还有一个好方法是将以上代码逐行debug观察执行过程,理解起来就简单的多了。

2.闭包形成条件

如果在一个函数内部,嵌套了函数,这个内部函数对外部作用域(非全局作用域)的变量进行引用,那么这个内部函数称为闭包。闭包每次运行时能记住引用的外部作用域的变量的值。

闭包的形成条件:

(1)一个函数嵌套了另一个函数;

(2)内层函数引用了外层函数的变量;

(3)外层函数将内层函数作为返回值返回。

3.自由变量

在上面的例子中,inner_1引用了外层函数outer_1的变量lst,一旦外部函数outer_1被执行,inner_1就成为了闭包函数,被内层函数引用的变量lst称为自由变量。

自由变量会与内层函数形成绑定关系,且是与闭包的具体实例关联,闭包的每个实例引用的自由变量互不干扰,这种互不干扰性我们再通过一个实例来了解下。

def outer():
    lst = []
    def inner(num):
        lst.append(num)
        print('lst:', lst)
    return inner
f1 = outer()
f2 = outer()

f2(1)
f1(2)
f2(2)

输出:
lst: [1]
lst: [2]
lst: [1, 2]

在这段代码中,通过f1 = outer(),f2 = outer(),我们生成两个闭包的实例,他们分别是f1,f2。

执行f2(1)时,执行闭包函数,向inner传递一个参数1,lst此时是空的,向其中添加一个元素1,所以此时lst的结果是[1]。

执行f1(1)时,执行闭包函数,向inner传递一个参数1,lst此时还是空的,向其中添加一个元素1,所以此时lst的结果是[1]。

执行f2(2)时,执行闭包函数,向inner传递一个参数2,因为已经执行了f2(1),lst已经是[1],再向其中添加一个元素2,所以此时lst的结果是[1,2]。

通过以上过程,我们可以验证并得出结论,lst在f1和f2中都存在,且他们之间互不干扰。

自由变量除了通过在outer外层函数中定义,还可以作为虚参,通过外层函数传入。请看下面的实例。

def outer(num1):
    def inner(num2):
        result = num1 + num2
        print('result:', result)

    return inner

f1 = outer(1)
f1(2)

输出:
result: 3

此时外层函数拥有一个虚参,在生成闭包实例时,需要相应向外层函数传入一个参数,这里传入参数值为1,被外层函数的接收后保存在num1中。在执行函数f1(2)时,等效于执行调用内部函数inner,因为inner也有一个参数,所以相应的在调用时也传入了一个参数,参数值为2,被内层函数的接收后保存在num2中。内层函数在执行时,num1是引用外层函数的num1,它的值为1,num2的值为2,相加后result的结果为3。最终输出result:3。

二、装饰器

1.装饰器基础

装饰器的本质就是对闭包的使用,它的作用是在不改变原有函数及函数名的基础上添加功能。

原则:1.不能修改被装饰的函数的源代码; 2.不能修改被装饰的函数的调用方式。

装饰器应用场景:

  • 引入日志
  • 函数执行时间统计
  • 执行函数前预备处理
  • 执行函数后清理功能
  • 权限校验等场景
  • 缓存

实例:我们有以下函数,通过添加一个装饰器,实现在原有输出内容的上下各添加一条分割线。

基础函数如下:

def f():
    print('---test---')
f()

我们采用闭包的方法来完成这个任务。

def wrapper(func):
    def inner():
        print("*****我是美丽分割线1*****")
        func()
        print("*****我是美丽分割线2*****")
    return inner
def f():
    print('---test---')
f = wrapper(f)
f()

我们看看使用装饰器是如何实现的:

def wrapper(func):
    print('正在装饰')
    def inner():
        print("*****我是美丽分割线1*****")
        func()
        print("*****我是美丽分割线2*****")
    return inner

@wrapper
def f():
    print('---test---')
f()

我们看下它的执行流程:

(1)def wrapper(func),定义装饰器函数,并将其载入内存。

(2)def f(),定义函数。此时发现在它的上方有@wrapper,@wrapper是调用装饰器函数,@wrapper将f作为参数传递到wrapper中,该句等效于f= wrapper(f)。

(3)此时wrapper(func)会立即执行,输出“正在装饰”,然后将inner会返回给f,此时f = inner()

(4)f()是执行函数,实际执行的是inner(),因此会输出"*****我是美丽分割线1*****",func()是对f()本体的调用,此时会输出'---test---',最后输出"*****我是美丽分割线2*****"。

我们发现@wrapper是对f=wrapper(f)的一种简化。

在实际使用的时候,一个装饰器可以对多个函数进行装饰,只要在每个被装饰函数的上方增加一句调用装饰器函数。

import time

def timer(func):
    def deco():
        start_time = time.time()
        func()
        stop_time = time.time()
        print('the func run time is %s'%(stop_time-start_time))
    return deco

@timer      #@timer 相当于就是test1 = timer(test1);此时test1=deco,deco里面的func=最开始的test1
def test1():
    time.sleep(3)
    print('in the test1')
@timer      #@timer 相当于就是test2 = timer(test2);此时test2=deco,deco里面的func=最开始的test2
def test2():
    time.sleep(5)
    print('in the test2')

test1()
test2()

2.多个装饰器

多个装饰器可以应用在一个函数上,装饰的顺序自下而上,而调用的顺序是自上而下。

def wrapper_out1(func):
    print('wrapper_out1正在装饰')
    def inner1():
        print("这里是inner1-1")
        func()
        print("这里是inner1-2")
    return inner1

def wrapper_out2(func):
    print('wrapper_out2正在装饰')
    def inner2():
        print("这里是inner2-1")
        func()
        print("这里是inner2-2")
    return inner2
@wrapper_out1
@wrapper_out2
def test():
    print("--test--")

test()

# 输出
wrapper_out2正在装饰
wrapper_out1正在装饰
这里是inner1-1
这里是inner2-1
--test--
这里是inner2-2
这里是inner1-2

执行过程:

(1)将wrapper_out1,wrapper_out2,test载入内存

(2)先对test进行装饰,因为test上面有两个装饰器,装饰按照自下而上原则,先执行@wrapper_out2,等效于test = wrapper_out2(test),输出'wrapper_out1正在装饰',然后将inner2传回给test;在执行@wrapper_out1,等效于test = wrapper_out1(test),注意此时括号里的test实际上是inner2,输出'wrapper_out1正在装饰',然后将inner1传回给test。这整个过程等效于:test = wrapper_out1(wrapper_out2(test)),括号里的test为原始test。

(3)调用test函数时,先执行inner1,输出:"这里是inner1-1",然后调用func(),这里的func实际是inner2,所以进入inner2,输出"这里是inner2-1",inner2中的func为test原函数,所以输出"--test--",然后输出"这里是inner2-2",inner2执行结束后,回到inner1中,输出"这里是inner1-2"。

3.对有参数和返回值的函数进行装饰

def func_dec(func):
    def wrapper(*args):
        print("average_value is %.2f."%(sum(args)/len(args)))
        result = func(*args)
        return result
    return wrapper

@func_dec
def add_sum(*args):
    return sum(args)

args = range(10)
result = add_sum(*args)
print(result)

# 输出
average_value is 4.50.
45

三、高级函数练习

1.装饰器基础练习

制作一个装饰器,实现在已有函数的字符串的上下各添加一段字符串,效果如下:

def test():
    print("这是第一次装饰器编程练习")
test()

# 输出
Hello world
这是第一次装饰器编程练习
I love python

2.使用计时器装饰函数

制作一个装饰器,实现在已有函数基础上,输出每个函数的运行时长。

def test1():
    time.sleep(5)
    print("这是第一次装饰器编程练习")

def test2():
    time.sleep(8)
    print("这是第二次装饰器编程练习")
test()

3.编写一个带参数的装饰器

def test(*args):
    print("这是第一次装饰器编程练习")
test('20010001','Tom')

# 输出
Id is 20010001,Name is Tom
这是第一次装饰器编程练习

四、第十六课字典练习答案

1.字典基本练习

dic = {
    'python': 95,
    'html/css': 99,
    'c': 100
}

'''
1.字典的长度是多少
2.请修改'html/css'这个key对应的value值为98
3.删除c这个key
4.增加一个key - value对,key值为php, value是90
5.获取所有的key值,存储在列表里
6.获取所有的value值,存储在列表里
7.判断javascript是否在字典中
8.获得字典里所有value的和
9.获取字典里最大的value
10.获取字典里最小的value
11.字典dic1 = {'php': 97}, 将dic1的数据更新到dic中
'''

print(len(dic))
dic['html/css'] = 98
del dic['c']
dic['php'] = 90
lst_key = list(dic.keys())
lst_value = list(dic.values())
print('javascript' in dic)
print(sum(dic.values()))
print(max(dic.values()))
print(min(dic.values()))
dic1 = {'php': 97}
dic.update(dic1)

2.找到b值

字典D中有17个键值对,每个键值对以(a,b):v的方式存储,要求编写一段程序,找出满足a==10,v==1条件时b的值

D = {(4, 7): 0, (2, 6): 1, (10, 4): 1, (5, 11): 1, (4, 5): 1,
(2, 8): 0, (8, 11): 0, (10, 0): 1, (6, 11): 1, (9, 11): 1,
(1, 9): 0, (10, 1): 0, (7, 11): 1, (0, 9): 1, (3, 7): 1,
(10, 3): 1, (10, 2): 1}

for a,v in D.items():
    if a[0] == 10 and v == 1:
        print(a[1])

3.词频统计

# 任务1:将所有大写转换为小写
words = words.lower()

# 任务2:生成单词列表
words_list = words.split()

# 任务3:生成词频统计
words_dict = {}
for word in words_list:
    if word in words_dict.keys():
        words_dict[word] += 1
    else:
        words_dict[word] = 1

# 任务4:排序
words_dict_list = list(words_dict.items())
words_dict_list.sort(key = lambda x:x[1],reverse=True)
print(words_dict_list)
# 任务5:排除语法型词汇,代词、冠词、连词
exclude = ["the", "has", "of", "a", "for", "and","away","from","on","to","are","only","have","in","after"]

for i in range(len(words_dict_list)-1,-1,-1):
    k = words_dict_list[i][0]
    if k in exclude:
        words_dict_list.pop(i)

# 任务6:输出词频最大TOP20
print(words_dict_list[:20])

4.探寻身份证号

身份证号包含着许多信息,第7到14位是生日信息,倒数第二位是性别代码,男性为奇数,女性为偶数。要求将account列表中的三个元素按照info字典的格式输出。

account = ['Jone360403200105070312', 'Tom36040320020903043X', 'Susan360101200108110181']
info = {}
for x in account:
    name = x[:len(x) - 18]
    year = x[-12:-8]
    month = str(int(x[-8:-6]))
    day = str(int(x[-6:-4]))
    sex = lambda x:'女' if int(x[-2]) % 2 == 0 else '男'
    info[name] = dict(性别=sex(x), 生日='%s年%s月%s日' % (year, month, day))
print(info)

猜你喜欢

转载自blog.csdn.net/qq_40407729/article/details/111876295