闭包和装饰器
一、魔法方法之__call__
在Python中,函数其实是一个对象,
所有的函数都是可调用对象,
一个类实例也可以变成一个可调用对象,只需要实现一个特殊方法__call__()
。
示例:
# 斐波那契数列
class Fib(object):
def __init__(self):
pass
def __call__(self,num):
a,b = 0,1;
self.l=[]
for i in range (num):
self.l.append(a)
a,b= b,a+b
return self.l
def __str__(self):
return str(self.l)
f = Fib()
print(f(10))
二、闭包
Python中函数也是对象,允许把函数本身作为参数传入另一个函数,还可以把函数作为结果值返回
,例如装饰器。
在函数内部再定义一个函数,并且内部函数用到了外部函数作用域里的变量(enclosing),那么将这个内部函数以及用到的外部函数内的变量
一起称为闭包
(Closure)
示例:
# 1. 错误的用法
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
# f1()、f2()和f3()的输出结果都是9,原因是调用这三个函数时,闭包中引用的外部函数中变量i的值已经变成3
# 2. 正确的用法
def count():
def f(j):
return lambda: j * j
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入闭包lambda: j * j
return fs
f1, f2, f3 = count()
print(f1()) # 输出1
print(f2()) # 输出4
print(f3()) # 输出9
注意: 闭包中不要引用外部函数中任何循环变量或后续会发生变化的变量
三、装饰器
装饰器(decorator)接受一个callable对象(可以是函数或者实现了call方法的类)作为参数,并返回一个callable对象
它经常用于有切面需求
的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
1. 被装饰的函数无参数
没有使用装饰器之前:
def f1():
print('function f1...')
def f2():
print('function f1...')
f1() # 输出function f1...
f2() # 输出function f2...
创建装饰器,接收函数参数,返回一个闭包函数inner:
def login_required(func):
def inner(): # inner是一个闭包,它使用了外部函数的变量func,即传入的原函数引用f1、f2...
if func.__name__ == 'f1': # 这里是权限验证的逻辑判断,此处简化为只能调用f1
print(func.__name__, ' 权限验证成功')
func() # 执行原函数,相当于f1()或f2()...
else:
print(func.__name__, ' 权限验证失败')
return inner
使用装饰器:
def f1():
print('function f1...')
def f2():
print('function f1...')
new_f1 = login_required(f1) # 将f1传入装饰器,返回inner引用,并赋值给新的变量new_f1
new_f1() # 执行函数,即执行inner(),这个闭包中使用的func变量指向原f1函数体
new_f2 = login_required(f2) # 将f2传入装饰器,返回inner引用,并赋值给新的变量new_f2
new_f2() # 执行函数,即执行inner(),func变量指向原f2,所以它不会通过权限验证,即不会执行func()
# 输出结果:
f1 权限验证成功
function f1...
f2 权限验证失败
上面使用装饰器有个问题,就是用户原来是调用f1()、f2()... ,现在你让他们调用new_f1()、new_f2()... , 这样肯定不行,所以需要修改如下:
f1 = login_required(f1) # 将f1引用传入装饰器,此时func指向了原f1函数体。返回inner引用,并赋值给f1,即现在是func指向原函数体,而f1重新指向了返回的inner闭包
f1() # 执行函数,即执行inner(),这个闭包中使用的func变量指向原f1函数体
上述两个步骤可以用@Python语法糖简写为:
# 1. 定义时
@login_required
def f1():
print('function f1...')
# 2. 调用时
f1()
2. 被装饰的函数有参数
示例:
def login_required(func):
def inner(a):
if func.__name__ == 'f1':
print(func.__name__, ' 权限验证成功')
func(a)
else:
print(func.__name__, ' 权限验证失败')
return inner
@login_required
def f1(a):
print('function f1, args: a=', a)
f1(10)
# 输出结果:
f1 权限验证成功
function f1, args: a= 10
调用f1(10)此时实际调用的是inner(10)
被装饰的函数有多个参数时:
def login_required(func):
def inner(*args, **kwargs):
if func.__name__ == 'f1':
print(func.__name__, ' 权限验证成功')
func(*args, **kwargs)
else:
print(func.__name__, ' 权限验证失败')
return inner
使用Python中的*args和**kwargs来匹配任意长度的位置参数或关键字参数
3. 被装饰的函数有返回值
示例:
def login_required(func):
def inner(*args, **kwargs):
if func.__name__ == 'f1':
print(func.__name__, ' 权限验证成功')
return func(*args, **kwargs)
else:
print(func.__name__, ' 权限验证失败')
return inner
@login_required
def f1(a, b, c):
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
# 输出结果:
f1 权限验证成功
function f1, args: a=10, b=20, c=30
hello, world
4. 装饰器带参数
像Flask的@route('/index')就是带参数的,其实route只是一个函数,它返回真正的装饰器,即在原来的装饰器外面再加一层函数.
示例:
def logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
print('[日志级别 {}]: 被装饰的函数名是 {}'.format(level, func.__name__))
return func(*args, **kwargs)
return wrapper
return decorator
@logging('DEBUG') # 等价于 f1 = logging('DEBUG')(f1) ,即先执行loggin('DEBUG'),返回decorator引用(真正的装饰器),再用decorator装饰f1,返回wrapper
def f1(a, b, c):
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
@logging('INFO')
def f2():
print('function f2...')
res = f1(10, 20, 30)
print(res)
f2()
# 输出结果:
[日志级别 DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world
[日志级别 INFO]: 被装饰的函数名是 f2
function f2...
5. 使用@wraps
示例:
from functools import wraps
def logging(level='INFO'):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""print log before a function."""
print('[日志级别 {}]: 被装饰的函数名是 {}'.format(level, func.__name__))
return func(*args, **kwargs)
return wrapper
return decorator
@logging('DEBUG')
def f1(a, b, c):
"""This is f1 function"""
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
print('正确的函数签名:', f1.__name__)
print('正确的函数文档:', f1.__doc__)
# 输出结果:
[日志级别 DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world
正确的函数签名: f1
正确的函数文档: This is f1 function
如果不加@wraps(func),调用f1(10, 20, 30),实际是调用装饰器中的wrapper(),所以打印出来的函数签名和文档都是wrapper的,所以使用functools模块的wraps装饰器解决这个问题。
6. 多个装饰器装饰同一个函数
示例:
# 装饰器1
def makeBold(func):
print('这是加粗装饰器')
def blod_wrapped():
print('---1---')
return '<b>' + func() + '</b>'
return blod_wrapped
# 装饰器2
def makeItalic(func):
print('这是斜体装饰器')
def italic_wrapped():
print('---2---')
return '<i>' + func() + '</i>'
return italic_wrapped
@makeBold
@makeItalic
def test():
print('---3---')
return 'Hello, world'
res = test()
print(res)
# 输出结果:
这是斜体装饰器 # 这在调用res = test()之前就会输出
这是加粗装饰器 # 这在调用res = test()之前就会输出
---1---
---2---
---3---
<b><i>Hello, world</i></b>
注意:
输出结果中,多个装饰器的顺序,包装时
,是从下往上
的,
而调用时
,相当于拆包装,肯定是从最外层开始拆
.
7. 基于类实现的装饰器
只要类实现了call方法,那么类实例化后的对象就是callable,即拥有了被直接调用的能力:
class Test():
def __call__(self):
print('call me!')
t = Test()
t() # 类实例化后的对象可以直接调用,输出:call me!
装饰器接受一个callable对象作为参数,并返回一个callable对象,那么我们可以让类的构造函数init ()接受一个函数,然后重载call ()并返回一个函数,也可以达到装饰器函数的效果:
class logging(object):
def __init__(self, func):
self._func = func
def __call__(self, *args, **kwargs):
print('[DEBUG]: 被装饰的函数名是 {}'.format(self._func.__name__))
return self._func(*args, **kwargs)
@logging
def f1(a, b, c):
"""This is f1 function"""
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
# 输出结果:
[DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world
带参数的类装饰器:
class logging(object):
def __init__(self, level='INFO'):
self._level = level
def __call__(self, func): # 接受函数
def wrapper(*args, **kwargs):
print('[日志级别 {}]: 被装饰的函数名是 {}'.format(self._level, func.__name__))
return func(*args, **kwargs)
return wrapper # 返回闭包
@logging('DEBUG')
def f1(a, b, c):
"""This is f1 function"""
print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
return 'hello, world'
res = f1(10, 20, 30)
print(res)
# 输出结果:
[日志级别 DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world
四、装饰器实例
插入日志:
from functools import wraps
import logging
def logged(level, name=None, message=None):
"""
Add logging to a function. level is the logging
level, name is the logger name, and message is the
log message. If name and message aren't specified,
they default to the function's module and name.
"""
def decorate(func):
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate
# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
print(add(3, 5))
spam()
# 输出结果:
2018-06-05 14:58:49,195 - test.py[line:19] - DEBUG: add
8
2018-06-05 14:58:49,237 - test.py[line:19] - CRITICAL: spam
Spam!