5.4 函数的特殊使用方式
5.4.1 匿名函数
所谓匿名函数,即不再使用def语句这样标准形式定义的函数。Python中可以使用lambda
关键字来创建匿名函数。用lambda创建的匿名函数的函数体比def
定义的函数体要简单。语法如下:
lambda [参数1[,参数2],....参数n]]:表达式
lam_sum = lambda arg1, arg2: arg1 + arg2
print(lam_sum(10, 20))
30
上述代码中,第一行定义了一个lambda函数,执行两个数的和运算,并且把该lambda函数命名为lam_sum。然后通过lam_sum()函数实现求和的功能。
Lambda创建的匿名函数中只能封装有限的逻辑进去。
lambda函数拥有自己的命名空间,且不能访问自有参数列表之外或全局命名空间里的参数。
实际上,一般在使用匿名函数时是不会再为创建的匿名函数命名的。因为这样失去了匿名函数的简便性。在有些场景是需要传入函数,需要的逻辑并不是很复杂。但是又不想再创建一个,这个时候就可以直接使用匿名函数了。如下:
print(list(map(lambda x: x * x, [1, 2, 3, 4, 5])))
[1, 4, 9, 16, 25]
5.4.2 递归调用
在Python定义函数时,函数体中可以调用其他函数,甚至可以调用自己。这种自己调用自己的方式叫做递归调用。下面是一个递归式函数定义:
def recursion():
return recursion()
显然,对于上面定义的函数,如果你运行它,你将发现运行一段时间后,这个程序崩溃了(引发异常)。
从理论上说,这个程序将不断运行下去,但每次调用函数时,都将消耗一些内存。因此函数调用次数达到一定的程度(且之前的函数调用未返回)后,将耗尽所有的内存空间,导致程序终止并显示错误消息“超过最大递归深度(maximum recursion depth exceeded,默认最大为1000次)”。
可以通过以下代码修改最大递归深度:
import sys
sys.setrecursionlimit(99999)
这个函数中的递归称为无穷递归(就像以 while True 打头且不包含 break 和 return 语句的循环被称为无限循环一样),因为它从理论上说永远不会结束。你想要的是能对你有所帮助的递归函数,这样的递归函数通常包含下面两部分。
基线条件:满足这种条件时函数将直接返回一个值。
递归条件:包含一个或多个调用,这些调用旨在解决问题的一部分。
这里的关键是,通过将问题分解为较小的部分,可避免递归没完没了,因为问题终将被分解成基线条件可以解决的最小问题。
那么如何让函数调用自身呢?这没有看起来那么难懂。前面说过,每次调用函数时,都将为此创建一个新的命名空间。这意味着函数调用自身时,是两个不同的函数[更准确地说,是不同版本(即命名空间不同)的同一个函数]在交流。你可将此视为两个属于相同物种的动物在彼此交流。
递归示例1:通过递归的方式求一个数的阶乘
def factorial(p_int=0):
if p_int == 0: # 基线条件
return 1
else: # 递归条件
return p_int * factorial(p_int - 1)
print(factorial(10))
3628800
递归示例2:通过递归的方式求幂
def power(x, n):
return 1 if n == 0 else x * power(x, n - 1)
print(power(2, 10))
1024
递归示例3:通过递归的方式解决汉诺塔问题
def move(n, a='A', b='B', c='C'):
if n == 1:
print('移动', a, '-->', c)
else:
move(n - 1, a, c, b)
move(1, a, b, c)
move(n - 1, b, a, c)
move(4)
移动 A --> B
移动 A --> C
移动 B --> C
移动 A --> B
移动 C --> A
移动 C --> B
移动 A --> B
移动 A --> C
移动 B --> C
移动 B --> A
移动 C --> A
移动 B --> C
移动 A --> B
移动 A --> C
移动 B --> C
在某些特殊的问题中,如果通过普通的循环方式虽然也可以实现,但在使用了递归的方式后代码更加简单。逻辑也更加清楚。
理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多会导致栈溢出。
5.4.3 偏函数
参考:偏函数
介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。
int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:
>>> int('12345')
12345
但int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换:
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565
假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
def int2(x, base=2):
return int(x, base)
这样,我们转换二进制就非常方便了:
>>> int2('1000000')
64
>>> int2('1010101')
85
functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85
所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
注意到上面的新的int2函数,仅仅是把base参数重新设定默认值为2,但也可以在函数调用时传入其他值:
>>> int2('1000000', base=10)
1000000
最后,创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数,当传入:
>>> int2 = functools.partial(int, base=2)
实际上固定了int()函数的关键字参数base,也就是:
>>> int2('10010')
相当于:
>>> kw = { 'base': 2 }
>>> int('10010', **kw)
当传入:
>>> max2 = functools.partial(max, 10)
实际上会把10作为args的一部分自动加到左边,也就是:
>>> max2(5, 6, 7)
相当于:
>>> args = (10, 5, 6, 7)
>>> max(args)
10
当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。
5.4.4 闭包
闭包就是一种函数的嵌套,首先定义了一个函数,称之为外部函数
。在这个外部函数体中又定义了一个内部函数
,并且这个内部函数体中使用到了外部函数的变量。外部函数最后return了内部函数。那么这个外部函数以及内部函数就构成了一个特殊的对象,称之为闭包。
闭包避免了使用全局变量,使得局部变量在函数外被访问成为可能,相比较面向对象,不用继承那么多的额外方法,闭包占用了更少的空间。
闭包示例
a = 1
def out_fun(b):
c = 3
def in_fun(d):
print(a + b + c + d)
return in_fun
infun = out_fun(2)
infun(4)
10
可以看到,在内部函数in_fun
中访问到了全局变量a、外部函数out_fun
的局部变量c以及参数b。
5.4.4.2 装饰器
装饰器(decorator)的本质:函数闭包(function closure)的语法糖(Syntactic sugar)。通过给函数装饰,可以加强函数的功能或者增加原本函数没有的功能。
装饰器在第一次调用被装饰函数时进行增强,并且只增强一次。
让我们先从一个简单的函数开始吧。假设现在有一个函数,用来计算从1到100累加之和,并输出结果。为了避免计算太快,我们在使用循环累加时设置等待0.01秒,函数定义如下:
def mysum1():
from time import sleep
total = 0
for _ in range(101):
sleep(0.01)
total += _
print(total)
mysum1()
5050
此时,如果我们想要知道调用这个函数执行一共花了多少时间,我们可以在执行前后获取时间再经过计算得到,也可以通过改造函数,在函数内部函数体前后获取时间计算得到。但是这两种方法都比较麻烦,尤其是第二种方法,需要侵入式修改函数代码。
这个时候就可以通过为函数添加装饰器来实现了。下面是创建装饰器的一般方法:
装饰器的简单定义
def decorator1(func):
def inner():
print('在这里执行被装饰函数执行前的增强操作')
func() # 执行被装饰的函数
print('在这里执行被装饰函数执行前的增强操作')
return inner
从上面可以看出,装饰器也是一个函数。只不过装饰器接收的参数是被装饰的函数。然后再装饰器内部定义一个函数,该内部函数体中执行要增强的操作代码以及执行被装饰的函数。最后再return该内部函数。
接下来是用装饰器来对某个函数进行装饰。我们以上面定义的mysum1函数来进行装饰:
装饰器的使用
@decorator1
def mysum1():
from time import sleep
total = 0
for _ in range(101):
sleep(0.01)
total += _
print(total)
mysum1()
在这里执行被装饰函数执行前的增强操作
5050
在这里执行被装饰函数执行前的增强操作
由上面可以看出,如果要装饰某个函数,只需要在定义这个函数时,在def语句的上一行添加@装饰器函数
即可。
对于装饰器装饰一个函数:
@decorator
def myfun():
print("hello")
上面的代码等价于:
def myfun():
print("hello")
myfun = decorator(myfun)
当一个装饰器装饰函数时,函数的功能增强了,因为在调用这个函数时,实际上调用的是在定义装饰器函数时,其内部函数。而此时内部函数是由增强功能命令和原被装饰函数组成。
创建一个统计函数运行时长的装饰器
import time
def decorator1(func):
def inner():
begin = time.time()
func() # 执行被装饰的函数
end = time.time()
print(f"函数`{func.__name__}`运行的总时间为:{end - begin:.3} 秒")
return inner
@decorator1
def mysum1():
from time import sleep
total = 0
for _ in range(101):
sleep(0.01)
total += _
print(total)
mysum1()
5050
函数mysum1
运行的总时间为:1.59 秒
5.4.4.2.2 被装饰函数接收参数
在上面的例子中,通过装饰器函数decorator
装饰的函数是不能有输入参数的,在实际使用中并不是很方便。
通过对装饰器进行改造可以避免这种情况,从而使装饰器函数有更广泛的用途。
装饰器定义:让被装饰函数接收参数
import time
def decorator2(func):
def inner(*args, **kwargs):
begin = time.time()
func(*args, **kwargs) # 执行被装饰的函数
end = time.time()
print(f"函数`{func.__name__}`运行的总时间为:{end - begin:.3}")
return inner
需要改造的地方:
1、为装饰器函数decorator
的内部函数inner
在定义时增加收集位置形参和收集关键字形参
2、在装饰器函数decorator
的内部函数inner
函数体中,执行被装饰器装饰的函数func
时,通过参数解包的方式传入参数。
装饰带有参数的函数:
@decorator2
def mysum2(a, b):
from time import sleep
total = a
for _ in range(total + 1, b + 1):
sleep(0.01)
total += _
print(total)
mysum2(1, 100)
5050
函数mysum1
运行的总时间为:1.56
5.4.4.2.3 装饰器函数接收参数
通过上面对装饰器进行改造,可以使的被装饰的函数可以输入参数。上面的装饰器函数decorator2
可以计算被装饰的函数执行时间,但是只能获取到执行一次的时间。如果想要通过参数获取执行任一次的时间,则需要使得装饰器可以接收参数。
装饰器定义:装饰器接收参数
import time
def decorator3(n):
def inner(func):
def wrapper(*args, **kwargs):
begin = time.time()
for _ in range(iteration):
func(*args, **kwargs)
end = time.time()
print(f"函数`{func.__name__}`运行的总时间为:{end - begin:.3}")
return wrapper
return inner
需要改造的地方:
1、装饰器函数此时并不是通过参数来传入被装饰的函数,而是定义装饰器自己的参数,演示时使用的是一个位置参数n,在后续如果遇到比较复杂的情况下也可以使用关键字形参、*args、**kwargs等收集参数。
2、内部函数inner
用来收集被装饰的函数。
3、内部函数inner
的内部函数wrapper
用来收集被装饰的函数的参数。并编写需要增强的命令。最终要执行被装饰的函数其实就是执行这个wrapper
函数。
装饰器接收参数:
import time
def decorator3(n):
def inner(func):
def wrapper(*args, **kwargs):
begin = time.time()
for _ in range(n):
func(*args, **kwargs)
end = time.time()
print(f"函数`{func.__name__}`运行的总时间为:{end - begin:.3}")
return wrapper
return inner
@decorator3(10)
def mysum3(a, b):
from time import sleep
total = a
for _ in range(total + 1, b + 1):
sleep(0.01)
total += _
print(total)
mysum3(1, 10)
# 等价于:mysum3 = decorator3(10)(mysum3)
55
55
55
55
55
55
55
55
55
55
函数mysum3
运行的总时间为:1.41
5.4.4.2.4 装饰器的返回值
如果你了解了上一节的内容,很容易想到只要在wrapper
函数中return就是被装饰函数的返回值。
装饰器定义:接收被装饰函数的返回值
import time
def decorator3(n):
def inner(func):
def wrapper(*args, **kwargs):
begin = time.time()
for _ in range(n):
func(*args, **kwargs)
end = time.time()
print(f"函数`{func.__name__}`运行的总时间为:{end - begin:.3}")
return end - begin
return wrapper
return inner
1、在上面的代码中,return end – begin就是被装饰器的返回值。可以通过变量进行接收。
@decorator3(3)
def mysum3(a, b):
from time import sleep
total = a
for _ in range(total + 1, b + 1):
sleep(0.01)
total += _
print(total)
total_time = mysum3(1, 10)
print(total_time)
函数
mysum3
运行的总时间为:1.42
1.4218323230743408
5.4.4.2.5 多个装饰器装饰同一个函数
对于某个函数,可以使用多个装饰器对其进行装饰,写法如下:
@decorator1
@decorator2
def 被装饰函数():
pass
对于被多个装饰器装饰的函数,其装饰顺序为由最近到远,即decorator2会先装饰,然后是decorator1。