python闭包和装饰器
1 魔法方法之__call__
在python中,函数也是一个对象
f = abs
print(f.__name__)
print(f(-123))
abs
123
由于 f 可以被调用,所以f 被称为可调用对象。所有的函数都是可调用对象。一个类实例也可以变成一个可调用对象,只需要实现一个特殊方法__call__()。
class Person(object):
def __init__(self,name,gender):
self.name = name
self.gender = gender
def __call__(self,friend):
print('My name is %s ...'%self.name)
print('My friend is %s ...'%friend)
p = Person('Bob','male')
p('Tim')
My name is Bob ...
My friend is Tim ...
单看 p('Tim') 你无法确定p是一个函数还是一个类实例,所以,在python中,函数也是对象,对象和函数的区别并不显著。可以把实例对象用类似函数的形式表示,进一步模糊了函数和对象之间的概念
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))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
在函数内部再定义一个函数,并且内部函数用到了外部函数作用域里的变量(enclosing),那么将这个内部函数以及用到的外部函数内的变量一起称为闭包(Closure)。装饰器(decorator)接受一个callable对象(可以是函数或者实现了call方法的类)作为参数,并返回一个callable对象,它经常用于有切面需求[1] 的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用
2 闭包
python中函数也是对象,允许把函数本身作为参数传入另一个函数,还可以把函数作为结果值返回。
在函数内部再定义一个函数,并且内部函数用到了外部函数作用域里的变量(enclosing),那么将这个内部函数以及用到的外部函数内的变量一起称为闭包(Closure):
def line(a,b):
def get_y_axis(x):
return a * x + b # 内部函数使用了外部函数的变量a和b
return get_y_axis # 返回值是闭包函数名,注意不是函数调用,没有小括号
L1 = line(1,1)# 创建一条直线:y = x + 1
print(L1)
print(L1(3))
L2 = line(2,3)# 创建一条直线:y = 2x+3
print(L2)
print(L2(3))
L3 = line(2,3)
print(L3)
print(L2 ==L3) # 每次调用line()返回的都是不同的函数,即使传入相同的参数
print(L2 is L3)
<function line.<locals>.get_y_axis at 0x0000000004DCFBF8>
4
<function line.<locals>.get_y_axis at 0x0000000004DF5158>
9
<function line.<locals>.get_y_axis at 0x0000000004DF5598>
False
False
注意:闭包中不要引用外部变量中任何循环变量或后续会发生变化的值
# 错误的做法
def count():
fs = []
for i in range(1,4):
def f():
return i*i
fs.append(f)# 注意:这里不是函数调用,而是将函数传递到列表中
return fs
f1,f2,f3 = count()
print(f1(),f2(),f3())# f1()、f2()和f3()的输出结果都是9,原因是调用这三个函数时,闭包中引用的外部函数中变量i的值已经变成3
9 9 9
# 正确的做法
def count():
def f(j):
return lambda:j*j
fs = []
for i in range(1,4):
fs.append(f(i))
return fs
f1,f2,f3 = count()
print(f1(),f2(),f3())
1 4 9
3 装饰器
装饰器(decorator)接受一个callable对象(可以是函数或者实现了call方法的类)作为参数,并返回一个callable对象
它经常用于有切面需求的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
举个实例,假设你写好了100个Flask中的路由函数,现在要在访问这些路由函数前,先判断用户是否有权限,你不可能去这100个路由函数中都添加一遍权限判断的代码(如果权限判断代码为5行,你得加500行)。那么,你可能想把权限认证的代码抽离为一个函数,然后去那100个路由函数中调用这个权限认证函数,这样只要加100行。但是,根据开放封闭原则,对于已经实现的功能代码建议不能修改,但可以扩展,因为可能你在这些路由函数中直接添加代码会导致原函数出现问题,那么最佳实践是使用装饰器。
3.1 被装饰的函数无参数
如果原来是调用f1()、f2() ... ,我们只要让用户还是调用f1()、f2() ... ,即他们调用的函数名还是保持不变,但实际执行的函数体代码已经变了(python中函数也是对象,函数名只是变量,可能改变它引用的函数体对象)
def f1():
print('function f1...')
def f2():
print('function f2...')
f1()
f2()
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
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()
function f1...
function f2...
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语法糖简写为:
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
# 1. 定义时
@login_required
def f1():
print('function f1...')
# 2. 调用时
f1()
f1 权限验证成功
function f1...
3.2 被装饰的函数有参数
def login_required(func):
def inner():
if func.__name__ == 'f1':
print(func.__name__, ' 权限验证成功')
func()
else:
print(func.__name__, ' 权限验证失败')
return inner
@login_required
def f1(a):
print('function f1, args: a=', a)
f1(10)
此时调用f1(10)实际上调用的是inner(10),而装饰器中的闭包inner没有定义参数,所以会报错
# 正确的做法
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)
正确的做法在两个地方都加入了参数,一个是inner()函数的定义中,一个是func()函数调用时。
现在这个装饰器能正确修饰一个参数的情况,但是遇到多个参数的时候还是需要重新定义,所以我们现在使用python中的*args和**kwargs来匹配任意长度的位置参数或关键字参数
def login_required(func):
def inner(*args, **kwargs):
if func.__name__ == 'f1':
print(func.__name__, ' 权限验证成功')
func(*args, **kwargs)
else:
print(func.__name__, ' 权限验证失败')
return inner
3.3 被装饰的函数有返回值
如果使用3.2的装饰器,修改f1()函数定义,它里面有return返回值,将会有问题
def login_required(func):
def inner(*args, **kwargs):
if func.__name__ == 'f1':
print(func.__name__, ' 权限验证成功')
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()
print(res)
f1 权限验证成功
function f1, args: a=10, b=20, c=30None
原因是调用f1(10, 20, 30),实际是调用inner(10, 20, 30),然后执行inner闭包的函数体,在执行到func(*args, **kwargs)后,没有接收原f1函数体的返回值。当inner闭包执行完毕,Python解释器也没有发现有return语句,就默认返回None
在inner中接收func函数的返回值,然后return返回它,本示例中装饰器的inner执行完func(*args, **kwargs)后没有其它代码了,所以可以直接修改为return func(*args, **kwargs),如果还有其它逻辑,则用变量保存func的返回值res = func(*args, **kwargs),inner最后一行返回return res
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
3.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...
3.5 使用@wraps
即使上面的做法已经很完美了,但是还是有瑕疵。实际上现在f1已经被装饰器重新赋值了,所以已经不是原来的f1了,当在外部查看函数的名字或者是文档时,就会发现错误
def logging(level='INFO'):
def decorator(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
错误的函数签名: wrapper
错误的函数文档: print log before a function.
原因是调用f1(10, 20, 30),实际是调用装饰器中的wrapper(),所以打印出来的函数签名和文档都是wrapper的,可以使用functools模块的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
3.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'
print()
res = test()
print(res)
这是斜体装饰器
这是加粗装饰器---1---
---2---
---3---
<b><i>Hello, world</i></b>
此时两个装饰器的情况就相当于下面的简化写法:
test = makeBold(makeItalic(test))
3.7 基于类实现的装饰器
只要类实现了__call__方法,那么类实例化后的对象就是callable,即拥有了被直接调用的能力
class Test():
def __call__(self):
print('call me!')
t = Test()
t() # 类实例化后的对象可以直接调用,输出:call me!
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
4 装饰器实例
4.1 函数执行时间
import time
from functools import wraps
def timethis(func):
'''
Decorator that reports the execution time.
'''
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)
return result
return wrapper
@timethis
def countdown(n):
'''Counts down'''
while n > 0:
n -= 1
return 'done'
res = countdown(100000)
print(res)
countdown 0.008999824523925781
done
内置的装饰器比如@staticmethod、@classmethod、@property原理也是一样的
4.2 插入日志
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)# logging.log(level, *args, **kwargs) 创建一条严重级别为level的日志记录
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()
2020-03-22 22:23:49,259 - <ipython-input-1-690b98a53a19>[line:19] - DEBUG: add
2020-03-22 22:23:49,334 - <ipython-input-1-690b98a53a19>[line:19] - CRITICAL: spam
8
Spam!