装饰器
1 闭包
2 什么是Python装饰器
3 为什么用装饰器?
4 在哪里用装饰器?
5 带参数的装饰器
6 内置装饰器
7 类装饰器
8 装饰器顺序
9 自定义属性的装饰器
#首先要先搞明白下面的代码:
#第一波
def foo():
print('foo')
foo #表示是函数
foo() #表示执行foo函数
#第二波
def foo():
print('foo')
foo = lambda x: x + 1
foo() # 执行下面的lambda表达式,而不再是原来的foo函数,因为foo这个名字被重新指向了另外一个匿名函数
#定义:
#是一个闭包,把一个函数当做参数,返回一个替代版的函数,本质上就是一个返回函数的函数
#简单的装饰器
def func1():
print("haha")
def outer(func):
def inner():
print("*******************")
func()
return inner
#f是函数func1的加强版本
f = outer(func1)
f()
------->
*******************
haha
- 闭包
如果在一个函数的内部定义了另一个函数,外部的我们叫做外函数,内部的我们叫做内函数。所以闭包的定义就是:
在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用。这样就构成了一个闭包。
一般情况下,在我们的认知当中,如果一个函数结束,函数内部所有东西会被释放掉,还给内存,局部变量会消失。但是闭包是一种特殊情况,如果外函数在结束时发现有自己的临时变量将来会在内部函数当中使用,就会把这个临时变量绑定给内部函数,然后自己再结束。
一般一个函数运行结束的时候,临时变量会被销毁。但是闭包是一个特别的情况。当外函数发现,自己的临时变量会在内函数中用到,自己再结束的时候,返回内函数的同时,会把外函数的临时变量同内函数绑定在一起。这样即使外函数已经结束了,内函数仍然能够使用外函数的临时变量。这就是闭包的强大之处。
#闭包函数的实例
# outer是外部函数 a和b都是外函数的临时变量
def outer( a ):
b = 10
# inner是内函数
def inner():
#在内函数中 用到了外函数的临时变量
print(a+b)
# 外函数的返回值是内函数的引用
return inner
if __name__ == '__main__':
# 在这里我们调用外函数传入参数5
#此时外函数两个临时变量 a是5 b是10 ,并创建了内函数,然后把内函数的引用返回存给了demo
# 外函数结束的时候发现内部函数将会用到自己的临时变量,这两个临时变量就不会释放,会绑定给这个内部函数
demo = outer(5)
# 我们调用内部函数,看一看内部函数是不是能使用外部函数的临时变量
# demo存了外函数的返回值,也就是inner函数的引用,这里相当于执行inner函数
demo() # 15
demo2 = outer(7)
demo2()#17
------>
15
17
外函数返回内函数的引用:
引用是什么?在Python中一切皆对象,包括整型数据1,函数,其实是对象。
当我们在进行 a = 1操作时,实际上内存中有一个地方存了值1,然后用a这个变量名存了1所在内存位置的引用。引用就好像C语言里的指针,大家可以把引用理解成地址。a只不过是一个变量名字,a里面存的是1这个数值所在的地址,就是a里面存了数值1的引用。
相同的道理,当我们在Python中定义一个函数def demo():的时候,内存中会开辟一些空间,存下这个函数的代码、内部的局部变量等等。这个demo只不过是一个变量名字,它里面存了这个函数所在位置的引用而已。我们还可以进行x= demo,y =demo,这样的操作就相当于,把demo里存的东西赋值给x和y,这样x和y都指向了demo函数所在的引用,在这之后我们可以用x()或者y()来调用我们自己创建的demo(),调用的实际上根本就是一个函数,x、y和demo三个变量名存了同一个函数的引用。
有了上面的解释,我们可以继续说,返回内函数的引用是怎么回事了。对于闭包,在外函数outer中最后return inner,我们再调用外函数demo = outer()的时候,outer返回了inner,inner是一个函数的引用,这个引用被存入了demo中。所以接下来我们再进行demo()的时候,相当于运行了inner函数。
同时我们发现,一个函数,如果函数名后紧跟一对括号,相当于现在我就要调用这个函数,如果不跟括号,相当于只是一个函数的名字,里面存了函数所在位置的引用。
外函数把临时变量绑定给内函数:
按照我们正常的认知,一个函数结束的时候,会把自己的临时变量都释放还给内存,之后变量都不存在了。一般情况下,确实是这样的。但是闭包是一个特别的情况。外部函数发现,自己的临时变量会在将来的内部函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量送给内函数绑定在一起。所以外函数已经结束了,调用内函数的时候仍然能够使用外函数的临时变量。
在编写的实例中,两次调用外部函数outer,分别传入的值是5和7.内部函数只定义了一次,我们发现调用的时候,内部函数是能识别外函数的临时变量是不一样的。Python中一切都是对象,虽然函数我们只定义了一次,但是外函数在运行的时候,实际上是按照里面代码执行的,外函数里创建了一个函数,我们每次调用外函数,它都创建一个内函数,虽然代码一样,但是却创建了不同的对象,并且把每次传入的临时变量数值绑定给内函数,再把内函数引用返回。虽然内函数代码是一样的,但其实,我们每次调用外函数,都返回不同的实例对象的引用,他们的功能是一样的,但是它们实际上不是同一个函数对象。
闭包中内函数修改外函数局部变量:
在基本的Python语法当中,一个函数可以随意读取全局数据,但是要修改全局数据的时候有两种方法:
1 global声明全局变量
2 全局变量是可变类型数据的时候可以修改
在闭包内函数也是类似的情况。在内函数中想修改闭包量(外函数绑定给内函数的局部变量)的时候:
在Python3中,可以用nonlocal关键字声明一个变量,表示这个变量不是局部变量空间的变量,需要向上一层变量空间找这个变量。
在Python2中,没有nonlocal这个关键字,我们可以把闭包变量改成可变类型数据进行修改,比如列表。
#修改闭包变量的实例
# outer是外部函数 a和b都是外函数的临时变量
def outer( a ):
b = 10 # a和b都是闭包变量
c = [a] #这里对应修改闭包变量的方法2
# inner是内函数
def inner():
#内函数中想修改闭包变量
# 方法1 nonlocal关键字声明
nonlocal b
b+=1
# 方法二,把闭包变量修改成可变数据类型 比如列表
c[0] += 1
print(c[0])
print(b)
# 外函数的返回值是内函数的引用
return inner
if __name__ == '__main__':
demo = outer(5)
demo() # 6 11
---->
6
11
# 使用闭包的过程中,一旦外函数被调用一次返回了内函数的引用,
# 虽然每次调用内函数,是开启一个函数执行过后消亡,但是闭包变量实际上只有一份,每次开启内函数都在使用同一份闭包变量
def outer(x):
def inner(y):
nonlocal x
x+=y
return x
return inner
a = outer(10)
print(a(1))
print(a(3))
------>
11
14
函数只是函数代码空间的引用,当函数名赋值给一个对象的时候就是引用传递
闭包就是一个嵌套定义的函数,在外层运行时才开始内层函数的定义,然后将内部函数的引用传递函数外的对象
内部函数和使用的外部函数提供的变量构成的整体称为闭包
2 什么是Python装饰器
如同字面意思,“装饰器”就是用来“装饰”Python的工具,使得代码更具有Python简洁的风格。换句话说,它是一种函数的函数,因为装饰器传入的参数是一个函数,然后通过实现各种功能来对这个函数的功能进行增强。
内裤是用来遮羞的,但是到了冬天它的功能可能已经无法用来遮挡风寒,所以人们就发明了长裤,秋裤等各种,保暖满足我们的需求,装饰器就相当于在内裤外面的长裤,没有改变内裤本身的功能,又给了我们增添保暖的作用。
装饰器的本质就是一个Python的函数,它在不需要更改任何代码的前提下增加额外的功能。
3 为什么用装饰器
前面提到了,装饰器是通过某种方式来增强函数的功能。当然,我们可以有很多种方式来增强函数的功能,只是装饰器有一个无法替代的优势:简洁。
我们只需要在每一个函数的上方加一个@就可以对这个函数进行增强。
# 如何理解装饰器
# 需求:
# 一个企业有N个业务部门,1个基础平台部门,基础平台负责提供底层的功能,
############### 基础平台提供的功能如下 ###############
def f1():
print('f1')
def f2():
print('f2')
def f3():
print('f3')
def f4():
print('f4')
############### 业务部门A 调用基础平台提供的功能 ###############
f1()
f2()
f3()
f4()
############### 业务部门B 调用基础平台提供的功能 ###############
f1()
f2()
f3()
f4()
# 第一版
############### 基础平台提供的功能如下 ###############
def f1():
# 验证1
# 验证2
# 验证3
print('f1')
def f2():
# 验证1
# 验证2
# 验证3
print('f2')
def f3():
# 验证1
# 验证2
# 验证3
print('f3')
def f4():
# 验证1
# 验证2
# 验证3
print('f4')
############### 业务部门不变 ###############
### 业务部门A 调用基础平台提供的功能###
f1()
f2()
f3()
f4()
### 业务部门B 调用基础平台提供的功能 ###
f1()
f2()
f3()
f4()
# 第二版
############### 基础平台提供的功能如下 ###############
def check_login():
# 验证1
# 验证2
# 验证3
pass
def f1():
check_login()
print('f1')
def f2():
check_login()
print('f2')
def f3():
check_login()
print('f3')
def f4():
check_login()
print('f4')
# 第三版
def w1(func):
def inner():
# 验证1
# 验证2
# 验证3
func()
return inner
@w1
def f1():
print('f1')
@w1
def f2():
print('f2')
@w1
def f3():
print('f3')
@w1
def f4():
print('f4')
from time import time, sleep
def fun_one():
start = time()
sleep(1)
end = time()
cost_time = end - start
print("func one run time {}".format(cost_time))
def fun_two():
start = time()
sleep(1)
end = time()
cost_time = end - start
print("func two run time {}".format(cost_time))
def fun_three():
start = time()
sleep(1)
end = time()
cost_time = end - start
print("func three run time {}".format(cost_time))
def run_time(func):
def wrapper():
start = time()
func() # 函数在这里运行
end = time()
cost_time = end - start
print("func three run time {}".format(cost_time))
return wrapper
@run_time
def fun_one():
sleep(1)
@run_time
def fun_two():
sleep(1)
@run_time
def fun_three():
sleep(1)
通过编写一个统计时间的装饰器run_time,函数的作为装饰器的参数,然后返回一个统计时间的参数wrapper,这就是装饰器的写法,也叫闭包,简单来说就是函数内嵌套函数。然后再每个函数上面加上run_time来调用这个装饰器对不同函数进行统计时间。
可见,统计时间这4行代码是重复的,一个函数需要4行,如果100个函数就需要400行,而使用装饰器,只需要几行代码实现一个装饰器,然后每个函数前面加一句命令即可,如果是100个函数,能少300行左右的代码量。
4 在哪里用装饰器?
装饰器最大的优势是处理重复性的操作,其主要的使用场景有如下几个:
1.计算函数的运行时间
2.给函数打日志
3.类型检查
4.插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计
当然,如果遇到其他重复操作的场景也可以类比使用装饰器。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概况的讲就是,装饰器的作用就是为已经存在的对象添加额外的功能。
import logging
def use_logging(func):
def wrapper():
logging.warning("%s is running" % func.__name__)
return func() # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
return wrapper
def foo():
print('i am foo')
foo = use_logging(foo) # 因为装饰器 use_logging(foo) 返回的是函数对象 wrapper,这条语句相当于
# foo = wrapper
foo() # 执行foo()就相当于执行 wrapper()
------>
WARNING:root:foo is running
i am foo
def use_logging(func):
def wrapper():
logging.warning("%s is running" % func.__name__)
return func()
return wrapper
@use_logging
def foo():
print("i am foo")
foo()
----->
WARNING:root:foo is running
i am foo
使用装饰器
-方法一:不用语法糖符号:@
-方法二:采用语法糖符号:@
use_logging 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数func包裹在其中,看起来像foo被use_logging装饰了一样,use_logging返回的也是一个函数,这个函数的名字叫wrapper。
@语法糖:
- @符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。
- 有了@,我们就可以省去foo = use_logging(foo)这一句了,直接调用foo()即可得到想要的结果。你们看到了没有,foo()函数不需要做任何修改,只需在定义的地方加上装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。
*args、**kwargs
可能有人问,如果我们的业务逻辑函数foo需要参数怎么办?比如:
def foo(name):
print("i am %s" % name)
我们可以在定义wrapper函数的时候指定参数:
def wrapper(name):
logging.warn("%s is running" % func.__name__)
return func(name)
return wrapper
这样foo函数定义的参数可以定义在wrapper函数中。这时,又有人要问了,如果foo函数接收两个参数呢?三个参数呢?更有甚者,我可能传很多个。当装饰器不知道foo到底有多少个参数时,我们可以用*args来代替:
def wrapper(*args):
logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper
如此一来,甭管foo定义了多少个参数,我都可以完整地传递到func中去。这样就不影响foo的业务逻辑了。这时还有读者会问,如果foo函数还定义了一些关键字参数呢?比如:
def foo(name, age=None, height=None):
print("I am %s, age %s, height %s" % (name, age, height))
这时,我们就可以把wrapper函数指定关键字参数:
def wrapper(*args, **kwargs):
# args是一个数组,kwargs一个字典
logging.warn("%s is running" % func.__name__)
return func(*args, **kwargs)
return wrapper
5 带参数的装饰器
装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数foo。装饰器的语法允许我们再调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了很大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。
def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
elif level == "info":
logging.info("%s is running" % func.__name__)
return func(*args)
return wrapper
return decorator
@use_logging(level="warn")
def foo(name='foo'):
print("i am %s" % name)
foo()
----->
WARNING:root:foo is running
i am foo
上面的use_logging是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我们使用@use_logging(level = “warn”)调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中。
@use_logging(level = “warn”)等价于@decorator
6 内置装饰器
常见的内置装饰器有三种,@property、@staticmethod、@classmethod
@property的使用
@property是Python的一种装饰器,是用来修饰方法的。@property是把类内方法当成属性来使用,使用@property装饰器来创建只读属性,@property装饰器会将方法转换为相同名称的只读属性,必须要有返回值,相当于getter:
只读属性就是只有@func.getter,没有定义@func.setter。
class Car:
def __init__(self, name, price):
self._name = name
self._price = price
@property
def car_name(self):
return self._name
# car_name可以读写的属性
@car_name.setter
def car_name(self, value):
self._name = value
# car_price是只读属性
@property
def car_price(self):
return str(self._price) + '万'
@staticmethod的使用
@staticmethod静态方法不能使用类变量和实例变量,装饰器的函数不传入self或者cls,则不能访问类属性和实例属性。
class cal:
cal_name = '计算器'
def __init__(self,x,y):
self.x = x
self.y = y
@property #在cal_add函数前加上@property,使得该函数可直接调用,封装起来
def cal_add(self):
return self.x + self.y
@classmethod #在cal_info函数前加上@classmethon,则该函数变为类方法,该函数只能访问到类的数据属性,不能获取实例的数据属性
def cal_info(cls): #python自动传入位置参数cls就是类本身
print('这是一个%s'%cls.cal_name) #cls.cal_name调用类自己的数据属性
@staticmethod #静态方法 类或实例均可调用
def cal_test(a,b,c): #改静态方法函数里不传入self 或 cls
print(a,b,c)
@classmethod的使用
@classmethod是用来指定一个类的方法为类方法,没有此参数指定的类的方法为实例方法,类方法的第一个参数时cls,而实例方法的第一个参数时self,表示该类的一个实例。
class A(object):
bar = 1
def foo(self):
print ('foo')
@staticmethod
def static_foo():
print('static_foo')
print(A.bar)
@classmethod
def class_foo(cls):
print('class_foo')
print(cls.bar)
cls().foo()
A.static_foo()
A.class_foo()
-------->
static_foo
1
class_foo
1
foo
@staticmethod和@classmethod区别
@staticmethod: 静态方法
@classmethod:类方法
一般来说,要使用某个类的方法,需要先实例化一个对象再调用方法。
而使用@staticmethod或@classmethod,就可以不需要实例化,直接通过类名就可以实现调用
使用:直接类名.方法名()来调用。@staticmethod和@classmethod都可以直接类名.方法名()来调用。
@staticmethod不需要表示自身对象的self和自身类的cls参数(这两个参数都不需要添加),就跟使用函数一样。
使用:直接类名.属性名或直接类名.方法名。
@classmethod也不需要self参数,但第一个参数需要是表示自身类的cls参数。
使用:直接类名.属性名或直接类名.方法名。
两者定义的装饰器调用方法一样,但是@classmethod装饰器定义的类方法需要传入类参数cls。
@staticmethod中要调用到这个类的一些属性方法,只能直接类名.属性名或类名.方法名。
而@classmethod有cls参数,可以来调用类的属性,类的方法,实例化对象等,避免硬编码更灵活。
- 不需要用到与类相关的属性和方法时,就用静态方法@staticmethod
- 需要用到与类相关的属性和方法时,然后又想表明这个方法时整个类通用的,不是对象特有的,就可以用类方法@classmethod
class A(object):
def foo(self, x):
print("executing foo(%s, %s)" % (self, x))
@classmethod
def class_foo(cls, x):
print("executing class_foo(%s, %s)" % (cls, x))
@staticmethod
def static_foo(x):
print("executing static_foo(%s)" % x)
a = A()
#通过实例调用方法,对象实例a作为第一个参数隐式传递。
a.foo (1)
#对于类方法,对象实例的类将隐式地作为第一个参数而不是传递self
a.class_foo(1)
#使用这个类调用class_foo
A.class_foo(1)
#对于staticmethods,self(对象实例)和cls(类)都不会作为第一个参数隐式传递。它们的行为类似普通函数,除了你可以从实例或类中调用它们
a.static_foo(1)
A.static_foo('hi')
print(a.foo)
print(a.class_foo)
print(a.static_foo)
print(a.static_foo)
------->
executing foo(<__main__.A object at 0x7f93738e6850>, 1)
executing class_foo(<class '__main__.A'>, 1)
executing class_foo(<class '__main__.A'>, 1)
executing static_foo(1)
executing static_foo(hi)
<bound method A.foo of <__main__.A object at 0x7f93738e6850>>
<bound method A.class_foo of <class '__main__.A'>>
<function A.static_foo at 0x7f9373a3f200>
<function A.static_foo at 0x7f9373a3f200>
7 类装饰器
没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大,高内聚,封装性等优点。使用类装饰器主要用类的call方法,当使用@形式将装饰器附加到函数上时,就会调用此方法。
class Foo(object):
def __init__(self, func):
self._func = func
def __call__(self):
print('class decorator runing')
self._func()
print('class decorator ending')
@Foo
def bar():
print('bar')
bar()
------->
class decorator runing
bar
class decorator ending
functools.wraps
使用装饰器极大地复用了代码,但是有一个缺点就是函数的元信息不见了,比如函数的docstring、name、参数列表,先看例子:
# 装饰器
def logged(func):
def with_logging(*args, **kwargs):
print(func.__name__) # 输出 'with_logging'
print(func.__doc__) # 输出 None
return func(*args, **kwargs)
return with_logging
# 函数
@logged
def f(x):
"""does some math"""
return x + x * x
logged(f)
--------->
<function __main__.logged.<locals>.with_logging(*args, **kwargs)>
不难发现,函数f被with_logging取代了,当然它的docstring,__name__就变成了with_logging函数的信息了。好在我们有functools.wraps, wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的func函数中,这使得装饰器里面的func函数也有和原函数func一样的元信息了。
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__name__) # 输出 'f'
print(func.__doc__) # 输出 'does some math'
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x
8 装饰器顺序
一个函数还可以同时定义多个装饰器,比如:
@a
@b
@c
def f():
pass
它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于:f = a(b(c(f)))
#装饰器的基本使用
#定义函数:完成包裹数据
def makeBold(fn):
def wrapped():
return "<b>" + fn() + "</b>"
return wrapped
#定义函数:完成包裹数据
def makeItalic(fn):
def wrapped():
return "<i>" + fn() + "</i>"
return wrapped
@makeBold
def test1():
return "hello world-1"
@makeItalic
def test2():
return "hello world-2"
@makeBold
@makeItalic
def test3():
return "hello world-3"
print(test1())
print(test2())
print(test3())
----->
<b>hello world-1</b>
<i>hello world-2</i>
<b><i>hello world-3</i></b>
9 自定义属性的装饰器
预先定义了装饰器run_time,它就会按照我们定义的流程去工作,只具备这一固定的功能,当然,我们前面介绍的通过带参数的装饰器让它具备了一定的灵活性,但是依然不够灵活。其实,我们还可以对装饰器添加一些属性,就如同给一个类定义实现不同功能的方法那样。
以输出日志为例,初学Python的同学都习惯用print打印输出信息,其实这不是一个好习惯,当开发商业工程时,你很用意把一些信息暴露给用户。在开发过程中,我更加鼓励使用日志进行输出,通过定义warning、debug、info等不同等级来控制信息的输出,比如info是可以让用户看的到的,让用户直接看程序跑到哪一个阶段了。debug是用于开发人员调试和定位问题时使用。warning是用于告警和提示。
那么问题来了,如果我们预先定义一个打印日志的装饰器,
def logger_info(func):
logmsg = func.__name__
def wrapper():
func()
log.log(logging.INFO, "{} if over.".format(logmsg))
return wrapper
logging.info是打印日志的等级,如果我们仅仅写一个基本的日志装饰器logger_info,那么它的灵活度太差了,因为如果我们要输出debug、warning等级的日志还得重写一个装饰器。
解决这个问题,有两个方法:
- 利用前面所讲的带参数的装饰器,把日志等级传入装饰器
- 利用自定义属性来修改日志等级
由于第一种已经以统计函数运行时间的方式进行了讲解,这里主要讲解第二种方法。
先看下代码:
import logging
from functools import partial
def wrapper_property(obj, func=None):
if func is None:
return partial(wrapper_property, obj)
setattr(obj, func.__name__, func)
return func
def logger_info(level, name=None, message=None):
def decorate(func):
logmsg = message if message else func.__name__
def wrapper(*args, **kwargs):
logging.log(level, logmsg)
return func(*args, **kwargs)
@wrapper_property(wrapper)
def set_level(newlevel):
nonlocal level
level = newlevel
@wrapper_property(wrapper)
def set_message(newmsg):
nonlocal logmsg
logmsg = newmsg
return wrapper
return decorate
@logger_info(logging.WARNING)
def main(x, y):
return x + y
main(3, 3)
main.set_level(logging.ERROR)
main(5, 5)
-------->
WARNING:root:main
ERROR:root:main
10
这里最重要的是wrapper_property这个函数,它的功能是把一个函数func编程一个对象obj的属性,然后通过调用wrapper_property,给装饰器添加了两个属性set_message和set_level, 分别用于改变输出日志的内容和改变输出日志的等级。