Python 中一个完整函数的样子:
@装饰器1
@装饰器2
...
def 函数名(位置形参, *元组形参, 命名关键字形参, **字典形参):
"""文档字符串"""
语句块
我们发现,函数的最上面是 @装饰器
,装饰器的本质其实是一个闭包函数。因此在开始介绍装饰器前,我们有必要学习一下闭包的概念。
一. 闭包 closure
闭包是指引用了此函数外部嵌套函数作用域变量的函数。闭包必须满足下面 3 个条件:
- 必须有内嵌函数;
- 内嵌函数必须引用外部函数中的变量;
- 外部函数返回值必须是内嵌函数。
下面,我们就以一个工作场景为例,一步步讲解闭包的由来。
场景假设:假设如下 decorated
函数,已经测试完毕并 release:
def decorated(x):
print('%d ** %d = %d' % (x, x, x**x))
运行结果:
>> decorated(2)
2 ** 2 = 4
>> decorated(3)
3 ** 3 = 27
现在提出新的需求:想在每次调用 decorated
函数前,均输出一次当前的时间,但前提是不允许修改已经 release 的 decorated
函数。
首先,想到的就是,重新定义一个新的函数 decorate
,并在该函数中调用 decorated
:
import time
def decorate(x):
print("%d:%d:%d" % time.localtime()[3:6])
decorated(x)
运行结果:
>> decorate(2)
21:23:8
2 ** 2 = 4
>> decorate(3)
21:23:13
3 ** 3 = 27
依然存在的问题:如果原程序中多处调用了 decorated
函数,则需把每一处都修改为 decorate
,工作量是一方面,如果你的 API 已经发布给外界的开发者使用,那将是件非常糟糕的事情。
继续尝试:在程序调用时无需修改原函数名。
在 Python 中,函数作为一等公民,因此可以像一个普通变量一样,作为参数传递给另一个函数,也可以作为函数的返回值:
import time
def decorate(func):
print("%d:%d:%d" % time.localtime()[3:6])
return func
这样,我们就可以在 decorated
函数被调用前,将其作为参数传递给 decorate
函数;并将 decorate
函数的返回值绑定到 decorated
变量:
>> decorated = decorate(decorated)
21:34:32
>> decorated(2)
2 ** 2 = 4
>> decorated(3)
3 ** 3 = 27
此方法可以实现在程序调用时,无需修函数名。
但仍存在一个非常严重的错误:decorated
函数本身并未做任何改变,因此运行结果中,decorate
函数只在 decorated = decorate(decorated)
赋值时打印一次时间。
闭包登场:为了解决上述需求和问题,下面请出闭包:
import time
def decorate(func):
def inner(x):
print("%d:%d:%d" % time.localtime()[3:6])
func(x)
return inner
上述代码中的 decorate
就是一个闭包了:decorate
函数内嵌了一个 inner
函数,且返回值为 inner
函数;并且 inner
函数访问了enclosing 作用域变量 func
,形成了一个闭包。
运行结果:
>> decorated = decorate(decorated)
>> decorated(2)
21:45:49
2 ** 2 = 4
>> decorated(3)
21:45:51
3 ** 3 = 27
由于执行 decorated = decorate(decorated)
语句之后,decorated
引用的就不再是原来的函数,而是闭包 decorated
的返回值 inner
函数。因此,后续调用 decorated
函数时,其实执行的都是 inner
函数。
目标完成:至此,我们使用闭包实现了在不修改函数内部及调用程序的情况下,改变了原函数的功能。
二. 装饰器
装饰器其实其实就是闭包的一个语法糖了,我们可以不用写诸如 decorated = decorate(decorated)
的赋值语句。只需要在 decorated
函数的最上方添加 @decorate
语句即可:
import time
def decorate(func):
def inner(x):
print("%d:%d:%d" % time.localtime()[3:6])
func(x)
return inner
@decorate
def decorated(x):
print('x ** x = %d' % x ** x)
运行结果:
>> decorated(2)
22:0:18
x ** x = 4
>> decorated(3)
22:0:23
x ** x = 27
装饰器的定义:装饰器是一个函数,主要作用是用来包装另一个函数和类。即在不改变原函数名(或类名)的情况下,改变被包装对象的行为。
什么是函数装饰器:函数装饰器是指装饰器是一个函数,传入的是一个函数,返回的也是一个函数。函数一旦被装饰,它绑定的将是装饰器函数的返回值。
语法结构:
def 装饰器函数名(函数名作为参数):
语句块
return 函数对象
@装饰器函数名
def 函数名(形参列表):
语句块
使用说明:
def decorator_fun(fn):
def fx():
print("[No1] 在函数 original_fun 调用之前,新增的处理程序!")
fn()
print("[No3] 在函数 original_fun 调用之后,新增的处理程序!")
return fx
@decorator_fun
def original_fun():
print("[No2] 函数 {} 正在被调用!".format(__name__))
运行结果:
>> original_fun()
[No1] 在函数 original_fun 调用之前,新增的处理程序!
[No2] 函数 __main__ 正在被调用!
[No3] 在函数 original_fun 调用之后,新增的处理程序!
关乎函数的几点说明:
- 函数本身可以赋值给变量,赋值后变量绑定函数;
- 允许将函数本身作为参数,传递给另一个函数;
- 允许函数返回一个函数;
我们应使用上述的函数式编程思想,而不是去修改函数内部,在程序中修改模块内部是大忌。
三. 形参
函数的4种形参定义方式为:
- 位置形参
- 星号元组形参
- 命名关键字形参
- 双星号字典形参
位置形参:传入位置实参。
def 函数名(形参名1, 形参名2, ...):
语句块
星号元组形参:允许传入任意个位置实参。
def 函数名(*元组形参名):
语句块
命名关键字形参:强制所有的传参都必须使用关键字传参。
def 函数名(*, 命名关键字形参)
语句块
def 函数名(*args,命名关键字形参)
语句块
双星号字典形参:允许传入任意个关键字实参。
def 函数名(**字典形参名):
语句块
以上就是 4 中形参的语法形式了,其中需要注意形参定义的顺序:位置形参、星号元组形参、命名关键字形参、双星号字典形参。
四. 文档字符串
函数可以包含自己的文档字符串,语法格式如下:
def 函数名(形参列表)
"""函数的文档字符串"""
函数语句块
示例:
def func():
'''此函数用来打招呼...
这是函数的文档字符串
'''
pass
可以在交互模式下输入如下命令,查看函数的文档字符串:
>> help(func)
Help on function func in module __main__:
func()
此函数用来打招呼...
这是函数的文档字符串
函数的 __doc__
属性也记录了函数的文档字符串:
>> print(func.__doc__)
此函数用来打招呼...
这是函数的文档字符串
此外,函数的 __name__
属性则记录了函数的名称:
>> print(func.__name__)
func
五. 实参
Python 中的函数有4种实参传递方式:
- 位置传参
- 序列传参(字符串,列表,元组)
- 关键字传参
- 字典关键字传参
位置传参:实参与形参的对应关系按位置来依次对应。
def func(a, b, c):
pass
func(1, 2, 3)
序列传参:函数在调用过程中用 *
将序列拆解后按位置进行传递的传参方式。
def func(a, b, c):
pass
s = "ABC"
L = [4, 5, 6]
t = (1.1, 1.2, 1.3)
func(*s)
func(*L)
func(*t)
关键字传参:按着形参的名称给形参赋值;实参和形参按名称进行匹配。
字典关键字传参:将字典用 **
拆解后进行关键字传参的传参方式。
def func1(a, b, *, c, d):
pass
def func2(a, b, *args, c, d):
pass
func1(1, 2, c=300, d=400)
func1(1, 2, **{'d':400, 'c':300})
func2(1, 2, 3, 4, c=300, d=400)
func2(1, 2, 3, 4, **{'d':400, 'c':300})