花了两天的时间把装饰器又重新学习了一下,主要参考了12步轻松搞定python装饰器和详解Python的装饰器这两篇博客的内容。本文把学习的内容和代码部分整理了一遍。
前面部分的内容比较简单,涉及到作用域、函数等一些基本概念。最初自己看的时候差点忽略想直接跳过去,后来看完整理笔记才体会到那些对理解装饰器还是有很大的帮助的。如果你看到这里,希望也能静下心看下去。
作用域
message = 'global message'
def foo():
# a_string = 'bbb'
print(locals())
print(message)
print(globals())
foo()
输出:
> # {... , 'message': 'global message', ...}
> # {}
> # global message
在python的作用域规则里面,创建变量一定会在当前作用域里创建一个变量,但是访问或者修改变量时会先在当前作用域查找变量,没有找到匹配变量的话会依次向上在闭合的作用域里面进行查看找。所以foo函数输出了message的内容。
message = 'global message'
def foo():
message = 'local message'
print(locals())
print(message)
print(globals())
foo()
输出:
# {... , 'message': 'global message', ...}
# {'message': 'local message'}
# local message
在当前作用域找到了变量之后,函数就不需要向上在闭包的作用域进行查找了。
嵌套函数
def outer():
x = 100
def inner():
print(x)
inner()
outer()
输出:
# 100
在print语句处发生了什么:python解释器需找一个叫x的本地变量,查找失败之后会继续在上层的作用域里面寻找,这个上层的作用域定义在另外一个函数里面。对函数outer来说,变量x是一个本地变量,但是如先前提到的一样,函数inner可以访问封闭的作用域(至少可以读和修改)
函数是python中的一级类对象
def foo():
pass
print(foo.__class__) # <class 'function'>
print(issubclass(foo.__class__, object)) # True
函数在python里面就是对象,可以把函数想参数一样传递给其他的函数或者说从函数了里面返回函数!
def my_add(x, y):
return x + y
def my_sub(x, y):
return x - y
def calc(func, x, y):
return func(x, y)
print(calc(my_add, 1, 2)) # 3
print(calc(my_sub, 1, 2)) # -1
calc的第一个参数准备接受一个函数的变量只是一个普通的变量而已,和准备接受其他类型的变量没有什么区别。把函数当做参数传入还是比较常见的,但是把函数当做返回值返回呢?
def outer():
def inner():
print('in inner')
return inner
foo = outer()
print(foo) # <function outer.<locals>.inner at 0x01736660>
foo() # in inner
在outer中,把inner作为返回值返回出来。这并没有什么特殊的语法,但是还记得变量的生存周期吗?每次函数outer被调用的时候,函数inner都会被重新定义,如果它不被当做变量返回的话,每次执行过后它将不复存在。从打印的信息也可以看出,outer()返回的确实是inner。
闭包
def outer():
x = 100
y = list()
z = dict()
def inner():
print(x, y)
return inner
foo = outer()
print(foo.__closure__) # (<cell at 0x01888C30: int object at 0x60A65F40>, <cell at 0x01888B70: list object at 0x017E7DA0>)
x是函数outer里的一个局部变量。当函数inner在#1处打印x的时候,python解释器会在inner内部查找相应的变量,当然会找不到,所以接着会到封闭作用域里面查找,并且会找到匹配。变量x是函数outer的一个本地变量,这意味着只有当函数outer正在运行的时候才会存在。根据我们已知的python运行模式,我们没法在函数outer返回之后继续调用函数inner,在函数inner被调用的时候,变量x早已不复存在,可能会发生一个运行时错误。但是我们可以看到程序运行并没有错误。Python支持一个叫做函数闭包的特性,嵌套定义在非全局作用域里面的函数能够记住它在被定义的时候它所处的封闭命名空间。这能够通过查看函数的closure属性得出结论,这个属性里面包含封闭作用域里面的值(只会包含被捕捉到的值,比如x和y,如果在outer里面还定义了其他的值,封闭作用域里面是不会有的,比如z)。
装饰器
def outer(func):
def inner():
print('before some_func')
ret = func()
return ret + 1
return inner
def foo():
return 1
decorated = outer(foo)
print(decorated())
输出:
# before some_func
# 2
装饰器其实就是一个闭包,把一个函数当做参数然后返回一个替代版函数。foo函数是最简单的版本,decorated就是装饰过的foo函数。
为什么需要装饰器
假设程序实现了两个函数如下:
def say_hello():
print('hello')
def say_goodbye():
print('hello')
上面两个函数都输出了hello,老板觉得有问题,要求调用每个方法前要记录进入函数的名称。
def say_hello():
print('[DEBUG]: enter say_hello()')
print('hello')
def say_goodbye():
print('[DEBUG]: enter say_goodbye()')
print('hello')
这当然太low了,当然还可以这么做
def debug():
import inspect
caller_name = inspect.stack()[1][3]
print('[DEBUG]: enter {}()'.format(caller_name))
def say_hello():
debug()
print('hello')
def say_goodbye():
debug()
print('hello')
是好一点了,但是每一个函数都要调用debug函数,还是很难受。
装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
概括的讲,装饰器的作用就是为已经存在的函数或对象添加额外的功能。
def wrapper(func):
def inner():
print('[DEBUG]: enter {}()'.format(func.__name__))
return func()
return inner
def say_hello():
print('hello')
say_hello = wrapper(say_hello)
say_hello()
wrapper函数对原函数进行了包装并返回了另外一个函数。
python支持了@语法糖之后,可以采用更简洁的写法。
@wrapper
def say_hello():
print('hello')
say_hello()
装饰器的传参问题
上面实现了一个最简单的装饰器,但是如果我们想要给被装饰的函数传入参数呢?只能让装饰器函数和被装饰的函数需要的参数保持一致。python提供的可变参数*args和**kwargs可以解决参数的问题。
def wrapper(func):
def inner(*args, **kwargs):
print('[DEBUG]: enter {}()'.format(func.__name__))
return func(*args, **kwargs)
return inner
@wrapper
def say_hello(*args, **kwargs):
print(*args)
say_hello('123', 1, 23)
实现高级一点的装饰器
带参数的装饰器。假设我们前文的装饰器需要完成的功能不仅仅是能在进入某个函数后打出log信息,而且还需指定log的级别,那么装饰器就会是这样的。
def logging(level):
def wrapper(func):
def inner_wrapper(*args, **kwargs):
print("[{level}]: enter function {func}()".format(
level=level,
func=func.__name__))
return func(*args, **kwargs)
return inner_wrapper
return wrapper
@logging(level='INFO')
def say(something):
print("say {}!".format(something))
@logging(level='DEBUG')
def do(something):
print("do {}...".format(something))
为什么需要用到三层函数的闭包。一般都是两层,第一层传入func,第二层传入参数。这里想要在装饰器传入一个参数,所以多加了一层,用来给装饰器传参。
如果没有使用@语法,等同于
say = logging(level='INFO')(say)
基于类的装饰器
装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了call()方法,那么这个对象就是callable的。
class Test():
def __call__(self, *args, **kwargs):
print('call me maybe!')
t = Test()
t() # call me maybe!
class logging():
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("[DEBUG]: enter function {func}()".format(
func=self.func.__name__))
return self.func(*args, **kwargs)
@logging
def say(something):
print(something)
say('hello')
带参数的类装饰器
如果需要通过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点。那么在构造函数里接受的就不是一个函数,而是传入的参数。通过类把这些参数保存起来。然后重载__call__
方法是就需要接受一个函数并返回一个函数。
class logging():
def __init__(self, level='INFO'):
self.level = level
def __call__(self, func):
def debug(*args, **kwargs):
print("[{level}]: enter function {func}()".format(
level=self.level,
func=func.__name__))
func(*args, **kwargs)
return debug
@logging(level='INFO')
def say(something):
print(something)
say('python')
内置装饰器
内置的装饰器和普通的装饰器原理是一样的,只不过返回的不是函数,而是类对象,所以更难理解一些。
class Test():
def getx(self):
return self._x # 必须使用_x,因为调用时传入x,进入函数后x实际是_x表示
def setx(self, value):
print('set')
self._x = value
def delx(self):
del self.x
x = property(getx, setx, delx, 'i am doc for x')
t = Test()
t.x = 12
print(t.x)
使用@语法糖实现的版本
class Test():
@property
def x(self):
print('get')
return self._x
@x.setter
def x(self, value):
print('set')
if not isinstance(value, int):
raise TypeError('not int')
self._x = value
@x.deleter
def x(self):
print('del')
del self._x
t = Test()
t.x = 12
print(t.x)
del t.x
使用装饰器之后原函数的签名等信息丢失
def logging(func):
def inner(*args, **kwargs):
'''print function name before do something'''
print('{} enter'.format(func.__name__))
return func(*args, **kwargs)
return inner
@logging
def say(something):
'''say something'''
print('say: {}'.format(something))
say('hello')
print(say.__name__) # inner
print(say.__doc__) # print function name before do something
解决办法
from functools import wraps
def logging(func):
@wraps(func)
def inner(*args, **kwargs):
'''print function name before do something'''
print('{} enter'.format(func.__name__))
return func(*args, **kwargs)
return inner
@logging
def say(something):
'''say something'''
print('say: {}'.format(something))
say('hello')
print(say.__name__)
print(say.__doc__)
不能装饰staticmethod或者classmethod
class Person():
def __init__(self, name, job):
self.name = name
self.job = job
@logging
def work(self):
print('job is {}'.format(self.job))
@logging
@staticmethod
def check_name(name):
if not isinstance(name, str):
print('name illegal')
else:
print('name legal')
程序会报错。@staticmethod返回的是一个staticmethod类对象,而@classmethod返回的是一个classmethod类对象。他们都是调用的是各自的__init__()
构造函数。装饰器必须接受一个callable对象,@staticmethod这个装饰器,其实它返回的并不是一个callable对象,而是一个staticmethod对象,那么它是不符合装饰器要求的(比如传入一个callable对象),你自然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod之前就好了,因为你的装饰器返回的还是一个正常的函数,然后再加上一个@staticmethod是不会出问题的,如下面代码所示。
class Person():
def __init__(self, name, job):
self.name = name
self.job = job
@logging
def work(self):
print('job is {}'.format(self.job))
@staticmethod
@logging
def check_name(name):
if not isinstance(name, str):
print('name illegal')
else:
print('name legal')