在学习 Python 的时候,庆幸自己有 JavaScript 的基础,在学习过程中,发现许多相似的地方,如导包的方式、参数解包、lambda 表达式、闭包、引用赋值、函数作为参数等。
装饰器是 Python 语言中的一个重点,要学习装饰器的使用,应该首先从闭包看起,以掌握装饰器的基本原理。
词法作用域
闭包是函数能够访问并记住其所在的词法作用域,由于 Python 和 JavaScript 都是基于词法作用域的,所以二者闭包的概念是共通的。
为了说明 Python 是基于词法作用域,而不是动态作用域,我们可以看下面的例子:
def getA():
return a
def test():
a = 100
print(getA())
test()
运行结果为:
Traceback (most recent call last):
File "C:\Users\Charley\Desktop\py\py.py", line 8, in <module>
test()
File "C:\Users\Charley\Desktop\py\py.py", line 6, in test
print(getA())
File "C:\Users\Charley\Desktop\py\py.py", line 2, in getA
return a
NameError: name 'a' is not defined
报错了~我们再在 getA
函数所在的作用域声明一个变量 a
:
def getA():
return a
a = 10010
def test():
a = 100
print(getA())
test()
运行结果为:
10010
这里输出了 10010
,说明 getA
函数是依赖于词法作用域的,其作用域在函数定义伊始就决定的,其作用域不受调用位置的影响。
理解了词法作用域,就理解了闭包。
闭包
前面说到过:闭包是函数能够访问并记住其所在的词法作用域的特性。那么只要函数拥有这个特性,这个函数就是一个闭包,理论上,所有函数都是闭包,因为他们都可以访问其所在的词法作用域。像这样的函数也是一个闭包:
a = 100
def iAmClosure():
print(a)
iAmClosure()
理论上是如此,但在实际情况下,闭包的定义要复杂一点点,但仍然基于前面的理论:如果一个函数(外层函数)中返回另外一个函数(内层函数),内层函数能够访问外层函数所在的作用域,这就叫一个闭包。
下面是一个例子:
def outer():
a = 100
def inner():
print(a)
return inner
outer()()
运行结果如下:
100
如上所示,inner
函数就是一个闭包。
闭包中调用参数函数
同 JavaScript,Python 中也可以将函数作为参数传递,由于 Python 是引用传值,因此实际上传入的参数并不是原始函数本身,而是原始函数的一个引用。基于闭包的特性,我们也可以在闭包中访问(或者说调用)这个函数:
def outer(fn):
def inner():
# 闭包能够访问并记住其所在的词法作用域
# 因此在闭包中可以调用 fn 函数
fn()
return inner
def test():
print("We will not use 'Hello World'")
ret = outer(test)
ret()
运行结果:
We will not use 'Hello World'
装饰器引入
认识了闭包,就可以来说一说装饰器了。想象有这么一种需求:
你所在的公司有一些核心的底层方法,后来公司慢慢壮大,增加了其他的部门,这些部门都有自己的业务,但它们都会使用这些核心的底层方法,你所在的部门也是如此。
有一天,项目经理找到你,让你在核心代码的基础上加一些验证之类的玩意,但是不能修改核心代码(否则会影响到其他的部门),你该怎么做呢?
首先你可能想到这种方式:
def core():
pass
def fixCore():
doSometing()...
core()
fixCore()
通过一个外层函数将 core
函数进行包装,在执行了验证功能后再调用 core
函数。
这时项目经理又说了,你不能改变我们的调用方式呀,我们还是想以 core
的方式进行调用,于是你又修改了代码:
def core():
pass
tmp = core;
def fixCore():
tmp()
core = fixCore
core()
通过临时函数 tmp
交换了 core
和 fixCore
,狸猫换太子。这下就可以愉快的直接使用 core
了。
这是项目经理又说了,我们需要对多个核心函数进行包装,总不能全部使用变量交换吧,并且这样很不优雅,再想想其他的办法?
好吧,要求真多!于是你想啊想,想到了闭包的方式:将需要包装的函数作为参数传入外层函数,外层函数返回一个内层函数,该函数在执行一些验证操作后再调用闭包函数。这样做的好处是:
- 可以对任意函数进行包装,只要将函数作为参数传入外层函数
- 可以在执行外层函数时对返回值进行任意命名
你写的代码是这个样子的:
# 外层函数,接收待包装的函数作为参数
def outer(fn):
def inner():
doSometing()...
fn()
return inner
# 核心函数1
def core1():
pass
# 核心函数2
def core2():
pass
# core1 重新指向被包装后的函数
core1 = outer(core1)
# core2 重新指向被包装后的函数
core2 = outer(core2)
# 调用核心函数
core1()
core2()
大功告成!简直完美。同时恭喜你,你已经实现了一个装饰器,装饰器的核心原理就是:闭包 + 函数实参。
Python 原生装饰器支持
在上面你已经实现了一个简单的装饰器,也知道了装饰器的基本原理。其实,在 Python 语言中,有着对装饰器的原生支持,但核心原理依旧不变,只是简化了一些我们的操作:
# 外层函数,接收待包装的函数作为参数
def outer(fn):
def inner():
print("----验证中----")
fn()
return inner
# 应用装饰器
@outer
# 核心函数1
def core1():
print("----core1----")
# 应用装饰器
@outer
# 核心函数2
def core2():
print("----core2----")
core1()
core2()
运行结果如下:
----验证中----
----core1----
----验证中----
----core2----
Python 原生的装饰器支持,省去了传参和重命名的步骤,应用装饰器时,会将装饰器下方的函数(这里为 core1
和 core2
)作为参数,并生成一个新的函数覆盖原始的函数。
装饰器函数的执行时机
装饰器函数(也就是我们前面所说的外层函数)在什么时候执行呢?我们可以进行简单的验证:
def outer(fn):
print("----正在执行装饰器----")
def inner():
print("----验证中----")
fn()
return inner
@outer
def core1():
print("----core1----")
@outer
def core2():
print("----core2----")
运行结果为:
----正在执行装饰器----
----正在执行装饰器----
这里我们并没有直接调用 core1
和 core2
函数,装饰器函数执行了。也就是说,解释器执行过程中碰到了装饰器,就会执行装饰器函数。
多重装饰器
我们也可以给函数应用多个装饰器:
def outer1(fn):
def inner():
print("----outer1 验证中----")
fn()
return inner
def outer2(fn):
def inner():
print("----outer2 验证中----")
fn()
return inner
@outer2
@outer1
def core1():
print("----core1----")
core1()
运行结果如下:
----outer2 验证中----
----outer1 验证中----
----core1----
从输出效果中可以看到:装饰器的执行是从下往上的,底层装饰器执行完成后返回函数再传给上层的装饰器,以此类推。
给被装饰函数传参
如果我们需要给被装饰函数传参,就需要在装饰器函数返回的 inner
函数上做文章了,让其代理接受被装饰器函数的参数,再传递给被装饰器函数:
def outer(fn):
def inner(*args,**kwargs):
print("----outer 验证中----")
fn(*args,**kwargs)
return inner
@outer
def core(*args,a,b):
print("----core1----")
print(a,b)
core(a = 1,b = 2)
运行结果为:
----outer 验证中----
----core1----
1 2
这里提一下 参数解包的问题:在 inner
函数中的 *
和 **
表示该函数接受的可变参数和关键字参数,而调用参数函数 fn
时使用 *
和 **
表示对可变参数和关键字参数进行解包,类似于 JavaScript 中的扩展运算符 ...
。如果直接将 args
和 kwargs
作为参数传给被装饰函数,那么被装饰函数接收到的只是一个元组和字典,所以需要在解包后传入。
对有返回值的函数进行包装
如果被包装函数有返回值,如何在包装获取返回值呢?先看一下下面的例子:
def outer(fn):
def inner():
print("----outer 验证中----")
fn()
return inner
@outer
def core():
return "Hello World"
print(core())
运行结果为:
----outer 验证中----
None
为什么函数执行的返回值是 None
呢?不应该是 Hello World
吗?这是因为装饰的过程其实是引用替换的过程,在装饰之前,core
变量指向其自初始的函数体,在装饰后就重新进行了指向,指向到了装饰器函数所返回的 inner
函数,我们没有给 inner
函数定义返回值,自然在调用装饰后的 core
函数也是没有返回值的。为了让装饰后的函数仍有返回值,我们只需让 inner
函数返回被装饰前的函数的返回值即可:
def outer(fn):
def inner():
print("----outer 验证中----")
return fn()
return inner
@outer
def core():
return "Hello World"
print(core())
运行结果如下:
----outer 验证中----
Hello World
装饰器的参数
有时候我们想要根据不同的情况对函数进行装饰,可以有以下两种处理方式:
- 定义多个不同条件下的装饰器,根据条件应用不同的装饰器
- 定义一个装饰器,在装饰器内部根据条件的不同进行装饰
第一种方法很简单,这里说一下第二种方式。
要在装饰器内部对不同条件进行判断,我们就需要一个或多个参数,将参数传入:
# main 函数接受参数,根据参数返回不同的装饰器函数
def main(flag):
# flag 为 True
if flag:
def outer(fn):
def inner():
print("立下 Flag")
fn()
return inner
return outer
# flag 为 False
else:
def outer(fn):
def inner():
print("Flag 不见了!")
fn()
return inner
return outer
# 给 main 函数传入 True 参数
@main(True)
def core1():
pass
# 给 main 函数传入 False 参数
@main(False)
def core2():
pass
core1()
core2()
运行结果如下:
立下 Flag
Flag 不见了!
上面我们根据给 main
传入不同的参数,对 core1
和 core2
函数应用不同的装饰器。这里的 main
函数并不是装饰器函数,其返回值才是装饰器函数,我们是根据 main
函数的返回值对目标函数进行装饰的。
类作为装饰器
除了函数,类也可以作为装饰器,在说类作为装饰器之前,首先需要了解 __call__
方法。
__call__ 方法
我们创建的实例也是可以调用的, 调用实例对象将会执行其内部的 __call__
方法,该方法需要我们手动实现,如果没有该方法,实例就不能被调用:
class Test(object):
def __call__(self):
print("我被调用了呢")
t = Test()
t()
运行结果:
我被调用了呢
类作为装饰器
我们已经知道对象的 __call__
方法在对象被调用时执行,其实类作为装饰器的结果就是将被装饰的函数指向该对象,在调用该对象时就会执行对象的 __call__
方法,要想让被装饰的函数执行 __call__
方法,首先会创建一个对象,因此会连带调动 __new__
和 __init__
方法,在创建对象时,test
函数会被当做参数传入对象的 __init__
方法。
class Test(object):
# 定义 __new__ 方法
def __new__(self,oldFunc):
print("__new__ 被调用了")
return object.__new__(self)
# 定义 __init__ 方法
def __init__(self,oldFunc):
print("__init__ 被调用了")
# 定义 __call__ 方法
def __call__(self):
print("我被调用了呢")
# 定义被装饰函数
@Test
def test():
print("我是test函数~~")
test()
运行结果:
__new__ 被调用了
__init__ 被调用了
我被调用了呢
保存原始的被装饰函数
装饰后的 test
函数指向了新建的对象,那么有没有办法保存被装饰之前的原始函数呢?通过前面我们已经知道,在新建对象的时候,被装饰的函数会作为参数传入 __new__
和 __init__
方法,因此我们可以在这两个方法中获取原始函数的引用:
class Test(object):
# 定义 __new__ 方法
def __new__(self,oldFunc):
print("__new__ 被调用了")
return object.__new__(self)
# 定义 __init__ 方法
def __init__(self,oldFunc):
print("__init__ 被调用了")
self.__oldFunc = oldFunc
# 定义 __call__ 方法
def __call__(self):
print("我被调用了呢")
self.__oldFunc()
# 定义被装饰函数
@Test
def test():
print("我是test函数~~")
test()
运行结果如下:
__new__ 被调用了
__init__ 被调用了
我被调用了呢
我是test函数~~
总结
本文主要讲到了 Python 中闭包和装饰器的概念,主要有以下内容:
- Python 是基于词法作用域
- 闭包是函数能记住并访问其所在的词法作用域
- 利用礼包实现简单的装饰器
- Python 原生对装饰器的支持
- 给函数应用多个装饰器
- 如何给被装饰函数传参
- 如何给有返回值的函数应用装饰器
- 如何根据不同条件为函数应用不同的装饰器
- 类作为装饰器的情况以及
__call__
方法
完。