> 文章列表 > 再度认识闭包和装饰器

再度认识闭包和装饰器

再度认识闭包和装饰器

之前总是为了学习而去学习,所以对一个知识点总是知其然而不知其所以然,如果在认识一个新的知识点的过程中没有疑问,那一定是没有掌握这个东西,甚至可以说是没有入门。这样的结果一定是似懂非懂,最后慢慢遗忘。闭包和装饰器在很早就了解过,并且还推过一篇文章,但是这几天被提及这个东西,我回答起来居然含糊其辞,没有逻辑。这是一件让人感到非常羞愧的事情。所以我打算用自己的语言再去陈述一遍我对这个东西的见解,也希望大家指出其中的不足或者帮忙扩展,一起进步!·

闭包

我们知道在python中一切皆对象,函数和类均是对象,它们可以赋值给一个变量,可以作为入参传递给函数,也可以作为函数的返回值,甚至可以添加到集合对象中去。闭包就是一种在函数内部定义一个新的函数,新函数使用了外部函数定义的变量,并且外部函数返回新函数的引用的方式。

def counter(start=0):def add_one():nonlocal start += 1return startretrn add_onec1 = counter(5)
print(c1())
print(c1())c2 = counter(5)
print(c2())
print(c2())# 结果:
# 6
# 7
# 6
# 7

当我们调用counter(5)时,返回的是add_one函数的引用,再次调用c1()运行add_one指向的代码块。

我们可以看到,闭包是数据+功能(函数)的结合,而我们常用的类,也是数据(属性)+功能(方法)的结合,类都需要继承object,所以相比较而言,闭包比类更轻量

对于一般的函数而言,在使用完变量后,会自动释放,而通过上例可以看出,在调用c1()后,并未释放内存,在下一次调用时依然使用的是原来定义的变量。闭包会携带包含它的函数的作用域,闭包间函数作用域互不影响,因此会比其它函数占用更多的内存,需要手动释放内存

内部函数只能引用外部函数的局部变量,如果需要修改外部函数的变量,需要使用关键字nonlocal

当函数、匿名函数、闭包、对象 当做实参时,有什么区别?

def test(temp):passdef a():pass# 相当于传递功能,不是传递数据
test(a)b = lambda x : x*2
# 相当于传递功能,不是传递数据
test(b)def person(name):def say(content):print(name, content)return sayp = person("xiaoming")
# 相当于传递了say这个功能以及name这个数据
test(p)class Persons(object):def __init__(self, name):self.name = namedef say(self,content):print(self.name,content)
#      
p2 = Persons("xiaohua")
# 相当于传递了功能以及name这个数据
test(p2)

闭包应用1:一个人站在原点,然后向X、Y轴进行移动,每次移动后及时打印当前的位置

def create():pos = [0,0]def player(direction, step):new_x = pos[0] + direction[0] * stepnew_y = pos[1] + direction[1] * steppos[0] = new_xpos[1] = new_yreturn playerplayer = create()
print(player([1,0], 10))
print(player([0,1], 20))
print(play([-1,0], 10))

闭包应用2:对某文件的特殊行进行分析,先要提取出这些特殊行

def make_filter(keep):def the_filter(filter_name):with open(file_name, 'r'):lines = file.readlines()file_doc = [i for i in lines if keep in i]return fileter_docreturn the_filterfilter = make_filter("163.com")
filter_result = filter("result.txt")

装饰器

引入

我们可以看以下一个例子,我们想要算出执行一个函数消耗了多少时间,不使用装饰器的情况下,可以这样实现:

import timedef calculate_10w():'''计算100000以内的每个数的立方和:return:'''sum_ret = 0for i in range(1, 100001):sum_ret += i  3print("10w以内的每个数的立方和为:",sum_ret)start_time = time.time()
calculate_10w()
stop_time = time.time()print("耗费总时长为:", stop_time - start_time, "(秒)")# 结果
# 10w以内的每个数的立方和为: 25000500002500000000
# 耗费总时长为: 0.04386019706726074 (秒)

如果我们需要对多个函数都单独计时,采用上述方法,每执行一个函数都需要在调用函数前后加上计时代码,然后执行一条输出时间语句,这样不仅会造成大量的重复代码,而且代码管理维护也会变得困难。可以观察到,除了需要执行的函数不一样,函数前后执行的语句都是相同的,那我们能不能定义一个方法,将函数作为入参,然后直接调用方法呢

import timedef calculate_10w():'''计算100000以内的每个数的立方和:return:'''sum_ret = 0for i in range(1, 100001):sum_ret += i  3print("10w以内的每个数的立方和为:",sum_ret)def caculate_time(fun):start_time = time.time()fun()stop_time = time.time()print("耗费总时长为:", stop_time - start_time, "(秒)")caculate_time(calculate_10w)# 结果
# 10w以内的每个数的立方和为: 25000500002500000000
# 耗费总时长为: 0.04386019706726074 (秒)

显然,重新定义一个函数,将需要计算执行时间的函数作为入参是可行的,也能解决重复代码的问题。但是计算函数执行时间只是我们想要实现的额外功能,运行函数calculate_10w()才是我们最主要的功能,通过上面的方法,calculate_10w()的执行完全隐藏在了caculate_time()函数当中,这样代码的可读性就大大降低了,对于后期代码的维护也没有很好。那还有方法可以更完美地解决这些问题吗?这个时候就引入了装饰器

装饰器是在不改变原函数或者类的功能的情况下,为函数/类添加额外的功能,它本身是个特殊的闭包函数或者类,入参是需要被装饰的函数或者类

import timedef caculate_time(fun):def inner():start_time = time.time()fun()stop_time = time.time()print("耗费总时长为:", stop_time - start_time, "(秒)")return inner@caculate_time
def calculate_10w():'''计算100000以内的每个数的立方和:return:'''sum_ret = 0for i in range(1, 100001):sum_ret += i  3print("10w以内的每个数的立方和为:",sum_ret)calculate_10w()# 结果
# 10w以内的每个数的立方和为: 25000500002500000000
# 耗费总时长为: 0.04386019706726074 (秒)

我们可以看到,通过上述方式,实现一个装饰器,然后在calculate_10w()函数上添加@caculate_time,再调用calculate_10w(),也能够实现计时器功能,那它是怎么做到的呢?

1、首先定义了一个入参为函数的闭包caculate_time(fun)函数,返回的是它内部的inner函数的引用

2、在需要添加计时功能的函数calculate_10w()上使用@caculate_time

3、调用calculate_10w()函数

4、当python解释器运行到caculate_time(fun)闭包函数时,会将函数加载到内存(只有被调用时才会被执行)

5、当执行到@caculate_time时,会将caculate_time看作可执行对象,去调用caculate_time()函数,并将被修饰的函数引用作为入参传入,将返回的inner引用赋值给calculate_10w,即

calculate_10w = caculate_time(calculate_10w)

5、当解释器执行calculate_10w()时,实际上是在运行inner函数

校验@的功能

import timedef caculate_time(fun):print("--------开始装饰----------")def inner():print("-----开始调用原函数------")start_time = time.time()fun()stop_time = time.time()print("耗费总时长为:", stop_time - start_time, "(秒)")print("------结束调用原函数------")print("-----完成装饰-------")return inner# @caculate_time
def calculate_10w():'''计算100000以内的每个数的立方和:return:'''sum_ret = 0for i in range(1, 100001):sum_ret += i  3print("10w以内的每个数的立方和为:",sum_ret)calculate_10w = caculate_time(calculate_10w)
calculate_10w()# 结果
# -------开始装饰----------
# ----完成装饰-------
# ----开始调用原函数------
# 0w以内的每个数的立方和为: 25000500002500000000
# 费总时长为: 0.04487943649291992 (秒)
# -----结束调用原函数------

函数装饰器

无参装饰器和有参装饰器

上述例子属于无参装饰器,装饰器不需要传入任何参数,如果我们需要使用参数,又不确定需要被装饰的函数的入参个数和类型,可以在装饰器的内函数中使用*args和kwargs作为入参。当调用原函数的时候,实参会传递到闭包中的内部函数的形参变量中,在内部函数执行的时候,将这些数据作为实参传递到原函数中

def timefun(func):def inner(*args, kwargs):func(*args, kwargs)return inner@timefun
def foo(a, b):print(a+b)@timefun
def count(a, b, c):print(a+b+c)foo(1,2)
count(1,2,3)# 结果
# 3
# 6

无返回值装饰器和有返回值装饰器

装饰器内部函数有return返回,则为有返回值装饰器,一般返回一个数据,也可以返回多个数据,相应地,装饰器内部函数没有return返回值,则是无返回值装饰器

def test(func):def inner(*args, kwargs):res1 = func(*args, kwargs)res2 = 5return res1,res2return inner@test
def myfunc(num):return numres1,res2 = myfunc(4)
print(res1)
print(res2)# 结果
# 4
# 5

装饰器传入参数

如果我们还希望装饰器也能够传入一些参数,可以这样做:

import timedef outter(timeout=0):def wrapper(func):def inner(*args, kwargs):print("开始执行函数")time.sleep(timeout)res = func(*args, kwargs)print("函数运行结束")return resreturn innerreturn wrapper@outter(3)
def test(num):return numres = test(5)
print(res)# 结果
# 开始执行函数
# 函数运行结束
# 5

执行@outter(3),得到wrapper引用,然后会执行test = wrapper(test),得到inner的引用;调用test(5),执行inner函数,得到返回值5

类装饰器

以上都是封装的函数装饰器,除了函数可以作为装饰器,类也能够作为装饰器来使用,基本的使用方式和函数装饰器类似。我们来看下面的例子:

class Test(object):def __init__(self,func):print("-----初始化------")print("func name is %s" % func.__name__)self.__func = funcdef __call__(self):print("-----装饰器中的功能----")self.__func()@Test
def mytest():print("----mytest----")mytest()# 结果# -----初始化------
# func name is mytest
# -----装饰器中的功能----
# ----mytest----

执行@Test时,相当于mytest = Test(mytest),即实例化了一个Test对象,并将mytest函数引用作为初始化参数传入__init__中,再将对象的引用赋值给mytest。当运行mytest()时,相当于调用了Test的实例对象,会自动调用并执行魔法函数__call__,从而运行原来的mytest函数

适用场景

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