函数进阶
函数的参数传递
Python的函数采用了引用传递的方法,即传递参数时,并不复制一份参数的内容,而是将参数的引用传递给函数。
例如:
def f(x):
return id(x)
a = 1.2
id(a)
4485901776
f(a)
4485901776
函数f(a)
的返回值与a的内存地址是一致的。这表示,当函数f()
被调用时,Python并没有将a的值复制一份传给参数x,而是让参数x与a共享了同一块内存。所以a和x是同一个对象。
对于列表有类似的结论:
b = [1, 2, 3]
f(b)
4486278400
id(b)
4486278400
共享同一个对象的机制意味着,可以在函数中修改传入参数的值。例如:
def mod_f(x):
x[0] = 999
return x
b
[1, 2, 3]
mod_f(b)
[999, 2, 3]
b
[999, 2, 3]
不过,如果在函数中给参数x赋了一个新值,如另一个列表,根据赋值机制,虽然x指向一个新的内存位置,但原来的变量不会改变:
def no_mod_f(x):
x = [4, 5, 6]
return x
no_mod_f(b)
[4, 5, 6]
b
[999, 2, 3]
默认参数的传递
有默认参数的情况下,Python会在函数定义时,预先为默认参数分配内存,以避免每次生成一个额外的默认参数。这样做能够节约一定的空间,不过也可能会得到一些与直觉不符的结果。例如:
def f(x = []):
x.append(1)
return x
理论上说,调用 f()
时返回的是 [1]
, 但事实上:
f()
[1]
f()
[1, 1]
f()
[1, 1, 1]
随着函数的调用,默认参数会被一直改变。这并不是Python的bug,且这种特性可以用于缓存。如果不想要这样的特性,可以这样定义函数:
def f(x = None):
if not x:
x = []
x.append(1)
return x
f()
[1]
f()
[1]
高阶函数
以函数作为参数,或者返回一个函数的函数都是高阶函数。在Python中,函数也是一种基本类型的对象,例如:
max
type(max)
builtin_function_or_method
对象性意味着可以对函数进行以下操作:
- 将函数作为参数传给另一个函数。
- 将函数名赋值给另一个变量。
- 将函数作为另一个函数的返回值。
例如:
def square(x):
"""Square of x."""
return x*x
def cube(x):
"""Cube of x."""
return x*x*x
可以将它们作为字典的值:
funcs = {
'square': square,
'cube': cube,
}
调用这些函数:
funcs['square'](3)
9
常见的高阶内置函数有 map()
和 filter()
。
map(f, sq)
将函数f作用在序列sq的每个元素上,得到结果组成的新序列。例如
map(square, range(5))
返回的结果是一个map迭代器,可以将其转换为列表:
list(map(square, range(5)))
[0, 1, 4, 9, 16]
filter(f, sq)
将函数f作用在序列sq的每个元素上,保留所有结果为True
的元素。例如:
def is_even(x):
return x % 2 == 0
filter(is_even, range(5))
list(filter(is_even, range(5)))
[0, 2, 4]
函数的返回值也可以是个函数。例如,定义一个返回函数的函数:
def power_func(num):
def func(x):
return x ** num
return func
平方和立方函数可以使用该函数定义出来:
square2 = power_func(2)
cube2 = power_func(3)
type(square2)
function
square2(10)
100
cube2(3)
27
Lambda表达式
Python提供了Lambda表达式,简化函数的定义,来定义一些匿名函数。
lambda <variables>: <expression>
Lambda表达式返回的是一个函数对象,接受<variables>
作为参数,返回<expression>
:
square3 = lambda x: x ** 2
cube3 = lambda x: x ** 3
type(square3)
function
square3(10)
100
cube3(3)
27
关键字global
在Python中,函数可以直接使用外部已定义好的变量值:
x = 15
def print_x():
print(x)
print_x()
15
如果想在函数中直接改x的值,需要加上关键字global
:
x = 15
def print_newx():
global x
x = 18
print(x)
print_newx()
18
x
18
如果不加,x的值不会改变:
x = 15
def print_newx():
x = 18
print(x)
print_newx()
18
x
15
递归
递归是指函数在执行的过程中调用了本身,一般用于分治法。例如,阶乘函数:
def fact(n):
return 1 if n == 0 else n * fact(n-1)
fact(6)
720
递归可以更快地实现代码,不过在效率上可能会有一定的损失。例如,斐波那契数列:
def fib1(n):
"""Fib with recursion."""
return 1 if n in (0, 1) else fib1(n-1) + fib1(n-2)
list(map(fib1, range(10)))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
非递归的方式的实现:
def fib2(n):
"""Fib without recursion."""
a, b = 1, 1
for i in range(1, n+1):
a, b = b, a+b
return a
list(map(fib2, range(10)))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
对这两个函数的运行时间进行比较:
%timeit -n 100 fib1(20)
2.98 ms ± 30.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit -n 100 fib2(20)
1.5 µs ± 20.1 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)
可以看到,两者的效率有很大的差别,非递归版本比递归版本要快很多,原因是递归版本中存在大量的重复计算。
为了减少递归中的重复,可以利用默认参数实现缓存机制:
def fib3(n, cache={0: 1, 1: 1}):
"""Fib with recursion and caching."""
try:
return cache[n]
except KeyError:
cache[n] = fib3(n-1) + fib3(n-2)
return cache[n]
list(map(fib3, range(10)))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
%timeit -n 100 fib3(20)
158 ns ± 49.9 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)