微信公众号: 码农充电站 pro
个人主页: https://codeshellme.github.io
当你选择了一种语言, 意味着你还选择了一组技术, 一个社区.
目录
本节我们来介绍闭包与装饰器.
闭包与装饰器是函数的高级用法, 其实在介绍完 Python 函数我们就可以介绍本节的内容, 但由于 Python 中的类也可以用来实现装饰器, 所以我们等到介绍完了 Python 类再来统一介绍闭包与装饰器.
装饰器使用的是闭包的特性, 我们先来介绍闭包, 再来介绍装饰器.
1, 什么是闭包
Python 的函数内部还允许嵌套函数, 也就是一个函数中还定义了另一个函数. 如下:
- def fun_1():
- def fun_2():
- return 'hello'
- s = fun_2()
- return s
- s = fun_1()
- print(s) # 'hello'
在上面的代码中, 我们在函数 fun_1 的内部又定义了一个函数 fun_2, 这就是函数嵌套.
我们在学习函数的时候, 还知道, Python 函数可以作为函数参数和函数返回值.
因此, 我们可以将上面代码中的函数 fun_2 作为函数 fun_1 的返回值, 如下:
- def fun_1():
- def fun_2():
- return 'hello'
- return fun_2
此时, 函数 fun_1 返回了一个函数, 我们这样使用 fun_1:
- fun = fun_1() # fun 是一个函数
- s = fun() # 调用函数 fun
- print(s) # s 就是'hello'
我们再来改进函数 fun_1, 如下:
- def fun_1(s):
- s1 = 'hello' + s
- def fun_2():
- return s1
- return fun_2
上面的代码中, 内部函数 fun_2 返回了变量 s1, 而 s1 是函数 fun_2 的外部变量, 这种内部函数能够使用外部变量, 并且内部函数作为外部函数的返回值, 就是闭包.
编写闭包时都有一定的套路, 也就是, 闭包需要有一个外部函数包含一个内部函数, 并且外部函数的返回值是内部函数.
2, 用闭包实现一个计数器
我们来实现一个计数器的功能, 先写一个框架, 如下:
- def counter():
- # 定义内部函数
- def add_one():
- pass
- # 返回内部函数
- return add_one
再来实现计数的功能, 如下:
- def counter():
- # 用于计数
- l = [0]
- # 定义内部函数
- def add_one():
- l[0] += 1
- return l[0] # 返回数字
- # 返回内部函数
- return add_one
上面的代码中, 我们使用了一个列表 l[0]来记录累加数, 在内部函数 add_one 中对 l[0]进行累加.
我们这样使用这个计数器:
- c = counter()
- print(c()) # 1
- print(c()) # 2
- print(c()) # 3
我们还可以使这个计数器能够设置累加的初始值, 就是为 counter 函数设置一个参数, 如下:
- def counter(start):
- l = [start]
- def add_one():
- l[0] += 1
- return l[0]
- return add_one
这样我们就可以使用 counter 来生成不同的累加器(从不同的初始值开始累加). 我们这样使用该计数器:
- c1 = counter(1) # c1 从 1 开始累加
- print(c1()) # 2
- print(c1()) # 3
- print(c1()) # 4
- c5 = counter(5) # c5 从 5 开始累加
- print(c5()) # 6
- print(c5()) # 7
- print(c5()) # 8
c1 从 1 开始累加, c5 从 5 开始累加, 两个互不干扰.
3, 什么是装饰器
装饰器是闭包的一种进阶应用. 装饰器从字面上理解就是用来装饰, 包装的. 装饰器一般用来在不修改函数内部代码的情况下, 为一个函数添加额外的新功能.
装饰器虽然功能强大, 但也不是万能的, 它也有自己适用场景:
缓存
身份认证
记录函数运行时间
输入的合理性判断
比如, 我们有一个函数, 如下:
- def hello():
- print('hello world.')
如果我们想计算这个函数的运行时间, 最直接的想法就是修改该函数, 如下:
- import time
- def hello():
- s = time.time()
- print('hello world.')
- e = time.time()
- print('fun:%s time used:%s' % (hello.__name__. e - s))
- # 调用函数
- hello()
其中, time 模块是 Python 中的内置模块, 用于时间相关计算.
每个函数都有一个__name__ 属性, 其值为函数的名字. 不管我们是直接查看一个函数的__name__ 属性, 还是将一个函数赋值给一个变量后, 再查看这个变量的__name__ 属性, 它们的值都是一样的(都是原来函数的名字):
- print(hello.__name__) # hello
- f = hello # 调用 f() 和 hello() 的效果是一样的
- print(f.__name__) # hello
但是, 如果我们要为很多的函数添加这样的功能, 要是都使用这种办法, 那会相当的麻烦, 这时候使用装饰器就非常的合适.
最简单的装饰器
装饰器应用的就是闭包的特性, 所以编写装饰器的套路与闭包是一样的, 就是有一个外部函数和一个内部函数, 外部函数的返回值是内部函数.
我们先编写一个框架:
- def timer(func):
- def wrapper():
- pass
- return wrapper
再来实现计时功能:
- import time
- def timer(func):
- def wrapper():
- s = time.time()
- ret = func()
- e = time.time()
- print('fun:%s time used:%s' % (func.__name__, e - s))
- return ret
- return wrapper
- def hello():
- print('hello world.')
该装饰器的名字是 timer, 其接受一个函数类型的参数 func,func 就是要修饰的函数.
func 的函数原型要与内部函数 wrapper 的原型一致(这是固定的写法), 即函数参数相同, 函数返回值也相同.
英文 wrapper 就是装饰的意思.
其实 timer 就是一个高阶函数, 其参数是一个函数类型, 返回值也是一个函数. 我们可以这样使用 timer 装饰器:
- hello = timer(hello)
- hello()
以上代码中, hello 函数作为参数传递给了 timer 装饰器, 返回结果用 hello 变量接收, 最后调用 hello(). 这就是装饰器的原本用法.
只不过, Python 提供了一种语法糖, 使得装饰器的使用方法更加简单优雅. 如下:
- @timer
- def hello():
- print('hello world.')
- hello()
直接在原函数 hello 的上方写一个语法糖 @timer, 其实这个作用就相当于 hello = timer(hello).
用类实现装饰器
在上面的代码中, 是用函数 (也就是 timer 函数) 来实现的装饰器, 我们也可以用类来实现装饰器.
用类实现装饰器, 主要依赖的是__init__ 方法和__call__ 方法.
我们知道, 实现__call__ 方法的类, 其对象可以像函数一样被调用.
用类来实现 timer 装饰器, 如下:
- import time
- class timer:
- def __init__(self, func):
- self.func = func
- def __call__(self):
- s = time.time()
- ret = self.func()
- e = time.time()
- print('fun:%s time used:%s' % (self.func.__name__, e - s))
- return ret
- @timer
- def hello():
- print('hello world.')
- print(hello())
其中, 构造方法__init__接收一个函数类型的参数 func, 然后,__call__方法就相当于 wrapper 函数.
用类实现的装饰器的使用方法, 与用函数实现的装饰器的使用方法是一样的.
4, 被修饰的函数带有参数
如果 hello 函数带有参数, 如下:
- def hello(s):
- print('hello %s.' % s)
那么装饰器应该像下面这样:
- import time
- def timer(func):
- def wrapper(args):
- s = time.time()
- ret = func(args)
- e = time.time()
- print('fun:%s time used:%s' % (func.__name__, e - s))
- return ret
- return wrapper
- @timer
- def hello(s):
- print('hello %s.' % s)
- hello('python')
timer 函数的参数依然是要被修饰的函数, wrapper 函数的原型与 hello 函数保持一致.
用类来实现, 如下:
- import time
- class timer:
- def __init__(self, func):
- self.func = func
- def __call__(self, args):
- s = time.time()
- ret = self.func(args)
- e = time.time()
- print('fun:%s time used:%s' % (self.func.__name__, e - s))
- return ret
- @timer
- def hello(s):
- print('hello %s.' % s)
- print(hello('python'))
不定长参数装饰器
如果 hello 函数的参数是不定长的, timer 应该是如下这样:
- import time
- def timer(func):
- def wrapper(*args, **kw):
- s = time.time()
- ret = func(*args, **kw)
- e = time.time()
- print('fun:%s time used:%s' % (func.__name__, e - s))
- return ret
- return wrapper
- @timer
- def hello(s1, s2): # 带有两个参数
- print('hello %s %s.' % (s1, s2))
- @timer
- def hello_java(): # 没有参数
- print('hello java.')
- hello('python2', 'python3')
- hello_java()
这样的装饰器 timer, 可以修饰带有任意参数的函数.
用类来实现, 如下:
- import time
- class timer:
- def __init__(self, func):
- self.func = func
- def __call__(self, *args, **kw):
- s = time.time()
- ret = self.func(*args, **kw)
- e = time.time()
- print('fun:%s time used:%s' % (self.func.__name__, e - s))
- return ret
- @timer
- def hello(s1, s2): # 带有两个参数
- print('hello %s %s.' % (s1, s2))
- @timer
- def hello_java(): # 没有参数
- print('hello java.')
- hello('python2', 'python3')
- hello_java()
5, 装饰器带有参数
如果装饰器也需要带有参数, 那么则需要在原来的 timer 函数的外层再嵌套一层函数 Timer,Timer 也带有参数, 如下:
- import time
- def Timer(flag):
- def timer(func):
- def wrapper(*args, **kw):
- s = time.time()
- ret = func(*args, **kw)
- e = time.time()
- print('flag:%s fun:%s time used:%s' % (flag, func.__name__, e - s))
- return ret
- return wrapper
- return timer
- @Timer(1)
- def hello(s1, s2): # 带有两个参数
- print('hello %s %s.' % (s1, s2))
- @Timer(2)
- def hello_java(): # 没有参数
- print('hello java.')
- hello('python2', 'python3')
- hello_java()
从上面的代码中可以看到, timer 的结构没有改变, 只是在 wrapper 的内部使用了 flag 变量, 然后 timer 的外层多了一层 Timer,Timer 的返回值是 timer, 我们最终使用的装饰器是 Timer.
我们通过函数.__name__ 来查看函数的__name__ 值:
- print(hello.__name__) # wrapper
- print(hello_java.__name__) # wrapper
可以发现 hello 和 hello_java 的__name__ 值都是 wrapper(即内部函数 wrapper 的名字), 而不是 hello 和 hello_java, 这并不符合我们的需要, 因为我们的初衷只是想增加 hello 与 hello_java 的功能, 但并不想改变它们的函数名字.
6, 使用 @functools.wraps
我们可以使用 functools 模块的 wraps 装饰器来修饰 wrapper 函数, 以解决这个问题, 如下:
- import time
- import functools
- def Timer(flag):
- def timer(func):
- @functools.wraps(func)
- def wrapper(*args, **kw):
- s = time.time()
- ret = func(*args, **kw)
- e = time.time()
- print('flag:%s fun:%s time used:%s' % (flag, func.__name__, e - s))
- return ret
- return wrapper
- return timer
- @Timer(1)
- def hello(s1, s2): # 带有两个参数
- print('hello %s %s.' % (s1, s2))
- @Timer(2)
- def hello_java(): # 没有参数
- print('hello java.')
此时, 再查看 hello 与 hello_java 的 __name__值, 分别是 hello 和 hello_java.
7, 装饰器可以叠加使用
装饰器也可以叠加使用, 如下:
- @decorator1
- @decorator2
- @decorator3
- def func():
- ...
上面代码的所用相当于:
decorator1(decorator2(decorator3(func)))
8, 一个较通用的装饰器模板
编写装饰器有一定的套路, 根据上文的介绍, 我们可以归纳出一个较通用的装饰器模板:
- def func_name(func_args):
- def decorator(func):
- @functools.wraps(func)
- def wrapper(*args, **kw):
- # 在这里可以使用 func_args,*args,**kw
- # 逻辑处理
- ...
- ret = func(*args, **kw)
- # 逻辑处理
- ...
- return ret
- return wrapper
- return decorator
- # 使用装饰器 func_name
- @func_name(func_args)
- def func_a(*args, **kw):
- pass
在上面的模板中:
func_name 是装饰器的名字, 该装饰器可以接收参数 func_args
内部函数 decorator 的参数 func, 是一个函数类型的参数, 就是将来要修饰的函数
func 的参数列表可以是任意的, 因为我们使用的是 * args, **kw
内部函数 wrapper 的原型 (即参数与返回值) 要与 被修饰的函数 func 保持统一
@functools.wraps 的作用是保留被装饰的原函数的一些元信息(比如__name__ 属性)
与装饰器相关的模块有 functools 和 wrapt, 可以使用这两个模块来优化完善你写的装饰器, 感兴趣的小伙伴可以自己拓展学习.
(完.)
来源: https://www.cnblogs.com/codeshell/p/13237874.html