![自学Python:编程基础、科学计算及数据分析](https://wfqqreader-1252317822.image.myqcloud.com/cover/254/34659254/b_34659254.jpg)
3.1 函数进阶
函数是Python的核心内容之一,能够提高代码的复用率。为了更好地使用函数,我们有必要了解函数的一些运行机制和高级用法。
3.1.1 函数参数传递
1. 参数的传递机制
在Python中,函数参数的传递机制与赋值机制一样,其本质都是共用内存地址,不会真正地复制对象。这种共享内存的方式叫做引用传递。
引用传递(Call By Reference)模式指的是函数在调用过程中,参数与实际传入的对象共享同一块内存地址。
为了进一步理解引用传递的本质,我们使用id()函数来进行简单说明。
定义如下函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/107_01.jpg?sign=1739162795-PWze3TMpVdLmIuA2hUEBo0cjkh8QRtWz-0-7e93ef7668be4c57bf4c352d54463473)
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/108_01.jpg?sign=1739162795-fEcvJOAthUCBvX2XOg47vqKkOftgLIOX-0-89c751e7bc158efabe1325ab738f5239)
对于给定的参数x,该函数返回出参数x的对应的内存地址。
我们将函数作用在一个对象上:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/108_02.jpg?sign=1739162795-ZBPotcywtEVZ2Uc3nvjidpBGy3WzkwCo-0-e52fc0800de956d34e086cb49203726b)
函数f(a)的返回值与a的内存地址是一致的。
当我们调用函数f时,Python并没有将a的值复制一份传给参数x,而是让参数x与a共享了同一块内存。也就是说,x和a是同一个对象。
再看传入列表作为参数的情况:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/108_03.jpg?sign=1739162795-Z7iBF9PmrpDzmceZh4cMNTCiZo3LDpTt-0-9670817950bdfe8402fd06ab8153e414)
共享同一个对象的机制意味着,我们可以在函数中修改传入参数的值。
例如,我们定义这样的一个函数,对于参数x,让x[0]变为999,并返回修改后的x:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/108_04.jpg?sign=1739162795-dQs2N8V7ROcrTxXJEABgLGUVFT4ikQQU-0-72c42d5d77a6130beb1bc8a07f71e190)
对列表a应用这个函数,Python会先让x指向a所在的内存。由于x和a共享同一个对象,修改x[0]会让a的值相应改变:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/108_05.jpg?sign=1739162795-qUa1fzRbpfdxe9L847DSQdoTBGs6hWa2-0-092af88d635fd0a23ce7cb48baeb5cf0)
假设我们在函数中给参数x赋了一个新值,如另一个列表。根据赋值机制,虽然x指向一个新的内存位置,但原来的变量不会改变:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/108_06.jpg?sign=1739162795-dyQs3VX4GPHJcIjmVf73YZsuVPHpHWne-0-7c08d8c6410e77445ec08a3a6f21a475)
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/109_01.jpg?sign=1739162795-7xa6WdmGuW7qqxJHuC5IyVUm7ud9Jtuf-0-7cc7561f44efa8cb650016ab3468f94f)
2. 默认参数的传递
有默认参数的情况下,Python会在定义函数时,预先为默认参数分配内存,每次调用默认参数时,Python会从这个预先分配好的内存地址得到这个默认参数,以免每次生成一个额外的默认参数。这样做能够节约一定的空间,不过也可能会得到一些与直觉不符的结果。
例如,考虑这样的函数f,该函数接受一个参数x,该参数的默认值为空列表:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/109_02.jpg?sign=1739162795-gli4p9Z5eqcbhP7IkkeUmszENKfN4hAR-0-538c43cf918835f8bc45efd4bdc3f27b)
如果我们不给f传递参数,直觉上它应该返回列表[1]。实际上,由于默认参数始终使用同一个列表,每次调用默认参数时,函数中的.append()方法改变了这个列表,因此,这个列表会随之改变:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/109_03.jpg?sign=1739162795-JLDkew7nJ5Vp6l9pD2XicW7ZKIrI7qcH-0-071d6624b6ff62cc94aa2e0621fdfb94)
因此,我们在使用默认参数时,应当尽量减少使用可变的容器类型,如列表、字典等作为函数的默认参数,以免出现意料之外的结果。
这种机制并不是Python设计上的问题,在某些情况下,这样的机制有时候还能方便我们进行操作,如提供对计算结果的缓存等。
3.1.2 高阶函数
以函数作为参数,或者返回一个函数的函数都是高阶函数(High-Order Function)。
1. 函数的对象性
在Python中,函数也是一种基本类型的对象,例如:
In [1]: max
Out[1]: <function max>
对象性意味着我们可以对函数进行以下操作:
● 将函数作为参数传给另一个函数;
● 将函数作为字典的值;
● 将函数作为另一个函数的返回值。
我们定义这样两个函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/109_04.jpg?sign=1739162795-T8P86LzlrGmWHbea3SIoHg54Sb6jWRHB-0-f72fc5127e5545c445f73872352fd750)
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/110_01.jpg?sign=1739162795-VG9fDyZgwXScHcv95zvY6kArcRfZT13L-0-fe3c60a883bcbddfb84c5c35f1a990ba)
把它们作为字典的值构造一个字典:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/110_02.jpg?sign=1739162795-cYCtAGEGqmesRhlc5WIZo0kl6mnCAlKf-0-c339e315f6da0556a7c3640915218219)
调用d["power3"](3)就相当于调用cube(3):
In [6]: d["power3"](3)
Out[6]: 27
2. 以函数为参数的函数
我们可以定义这样一个函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/110_03.jpg?sign=1739162795-bYWfrGoMSYAH0oz8bj2ggcazSIV5nXwq-0-5dd8f73594ac17ced1923c58212da0e9)
该函数接受两个参数:一个函数f和一个序列sq,其作用是将函数f作用在序列sq的每个元素上。由于参数f是一个函数,所以我们定义的这个函数是一个高阶函数。
Python中已经有map()函数实现同样的功能,其用法为:
map(f, sq)
其作用是将函数f作用到sq的每一个元素上去,并返回所有结果组成的列表。例如,可以将之前定义的函数square()应用到一个序列上去:
In [7]: map(square, range(5))
Out[7]: [0, 1, 4, 9, 16]
3. 以函数为返回值的函数
函数的返回值也可以是个函数。
Python支持在函数中定义子函数,我们定义一个函数,利用子函数将平方和立方函数一般化:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/110_04.jpg?sign=1739162795-pQ9tVaY1rhDHdRJCFSB7NfnjqrEPbzEz-0-af6f7c80aa073eab9d93d1e281663e0b)
该函数接受一个参数num,并定义了一个子函数func(),最终返回定义的这个子函数。在子函数func()中,我们使用了传入的num参数。
有了这个函数,平方和立方函数可以这样定义:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/111_01.jpg?sign=1739162795-48JOmoy5cipkYleAHlabK26zBOxnHN2L-0-20405733e05a6be1436239859116571a)
4. 固定部分参数的函数
Python的functools模块提供了一个叫partial()的函数,它接受一个函数作为参数,并返回一个将这个函数的一部分参数固定的新函数。
首先导入这个函数:
In [13]: from functools import partial
定义一个计算幂的函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/111_02.jpg?sign=1739162795-kmw6onOI1Xf3gX5AuQnzJ8G6PNBMLndb-0-1438a4af9056aca4e074262f57a4370b)
该函数接受两个参数:x和它的幂次num。
使用partial()函数定义平方:
In [15]: square3 = partial(power, num=2)
partial()函数的第一个参数是需要固定参数的函数,后面是函数中需要固定的参数值。需要固定的参数可以采用键值对的形式指定:如“num=2”。
通过固定参数值,我们定义了一个返回值为x ** 2的函数,即平方函数:
In [16]: square3(4)
Out[16]: 16
类似地,可以让num参数固定为3,定义立方函数如下:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/111_03.jpg?sign=1739162795-bGje0HA0kBDQ2NeAic43MrpGVf9gvUK1-0-b1df36f3d7ef8a28b8bde53d8975074c)
在partial()函数中,除了键值对的形式,需要固定的参数值也可以不指定名称,按照函数定义的顺序依次固定各个参数。
例如,对于下面的用法:
partial(power, 2)
partial()函数会将power()函数的第一个参数x固定为2,所以,相当于生成了一个计算2的n次方的函数:
In [19]: partial(power, 2)(3)
Out[19]: 8
3.1.3 函数map()、filter()和reduce()
map()函数接受一个函数f和一个序列sq:
map(f, sq)
其作用是将函数f作用在序列的所有元素上,等价于:
[f(x) for x in sq]
filter()函数也接受一个函数f和一个序列sq:
filter(f, sq)
其作用是通过函数f来筛选序列中的元素,等价于:
[x for x in sq if f(x)]
比如一个判断偶数的函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/112_01.jpg?sign=1739162795-3sXeZeVE3ETzGiN7FKLRx7CmFbczok0a-0-21405a11c78e2bcacdda6fde8b635462)
使用filter()函数,可以得到序列中的所有偶数:
In [2]: filter(is_even, range(5))
Out[2]: [0, 2, 4]
还可以将map()函数和filter()函数一起使用,求序列中所有偶数的平方:
In [3]: map(square, filter(is_even, range(5)))
Out[3]: [0, 4, 16]
相当于:
[square(x) for x in range(5) if is_even(x)]
另一个常用的高阶函数为reduce()函数,其基本用法为:
reduce(f, sq)
与map()函数和filter()函数不同,reduce()函数接受的是一个支持二元操作的函数:f(x, y),实现对序列sq中的元素累加计算,并返回单一的结果。
定义一个加法函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/112_02.jpg?sign=1739162795-bkIT6yKWoxUh9Js4OVxjnuFC6wCyviMQ-0-bd4687ea350417b663f6de4f8c6fdc14)
将该函数作为reduce()函数的参数使用:
In [5]: reduce(add, [1,2,3,4,5])
Out[5]: 15
这种用法相当于进行了一个这样的一个计算:((((1+2)+3)+4)+5)。第一步计算的输入参数是序列的前两个元素,并将得到的结果与下一个元素进行计算,直到最后一个元素。
reduce()函数还可以提供初始值:
In [6]: reduce(add, [1,2,3,4,5], 10)
Out[6]: 25
给定初始值时,第一步计算的输入参数是初始值10与序列的第一个元素1,而不是序列的前两个元素1和2。
3.1.4 Lambda表达式
在使用函数作为参数的时候,如果传入的函数比较简单或者使用次数较少,在文件中直接定义这些函数就显得比较浪费。为此,Python提供了Lambda表达式(Lambda Expression)来简化函数的定义。
Lambda表达式用关键字lambda定义,其基本形式为:
lambda <variables>: <expression>
Lambda表达式返回的是一个函数对象,其中<varibales>是该函数的参数,<expression>则是函数的返回值,用冒号“:”进行分割。
Lambda表达式在定义时只有参数和返回值,并没有显示函数的名称。对于一些简单的函数,这样做可以省去定义函数的麻烦。
例如,平方函数可以用Lambda表达式定义:
In [1]: lambda x: x ** 2
Out[1]: <function __main__.<lambda>>
将它传入map()函数作为参数:
In [2]: map(lambda x: x ** 2, range(5))
Out[2]: [0, 1, 4, 9, 16]
用Lambda表达式作为filter()函数的输入,判断是否为偶数:
In [3]: filter(lambda x: x % 2 == 0, range(5))
Out[3]: [0, 2, 4]
用Lambda表达式和reduce()函数求和:
In [4]: reduce(lambda x, y: x + y, range(5))
Out[4]: 15
其中,lambda x, y: x+y表示函数接受x和y两个变量,并返回变量x和y的和。
可以直接用Lambda表达式进行赋值,得到的对象是个函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/114_01.jpg?sign=1739162795-hwSLt4qM02HGaWp16gRs4YeVKBWhWp3f-0-181114bf68f610b717a1e5bc88040f26)
3.1.5 关键字global
我们可以在函数中直接使用外部变量的值。
例如,在函数foo()中,我们打印出外部变量x的值:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/114_02.jpg?sign=1739162795-FlChmtFxvymXnnacq5HSGwK35R8CKHRP-0-d34bccfdd8dadfca7feb989fcebd3b64)
不过,如果我们在函数中,给外部变量x赋值,外面的x不会变化:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/114_03.jpg?sign=1739162795-T2xf9f9ESrJk9ItQfznTK2WWhBsq4EKp-0-0a8b4375a2c18852da143c29951a8de6)
foo函数打印的x是18,但外部变量x仍然是15。在函数里直接给x赋值不会影响外面的变量x。
在函数中,我们可以使用关键字global来给外部变量重新赋值,只需要在函数中用global声明一下外部变量x,并在函数里给x赋值:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/114_04.jpg?sign=1739162795-HuPa1JMkIseiu0BVfAEBhkTOlnfPKltc-0-417db9cb469bf5bdc466f4b53920ad9e)
此时,调用foo()会改变外部变量x的值:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/114_05.jpg?sign=1739162795-ukmA9aw2GMEP90jlfoRzWt1hXofbycoU-0-61bb9c00ccb4aa3c59b543d6ba126939)
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/115_01.jpg?sign=1739162795-VHIQv2fj6VrLaI3OSiyEDa6AgULloTEl-0-ea2be9280c60e0559c73491102ae9372)
3.1.6 函数的递归
递归(Recursion)是指函数在执行的过程中调用了本身,通常用于分治法(Divide And Conquer),即将问题拆分为规模较小的子问题。
例如,阶乘函数可以写成:
f (n) = n! = n×(n-1)! = n×f (n-1)
我们把求解n阶乘的问题变成了一个求解n-1阶乘的问题,以此类推,我们只需要解决最简单的f(1)的问题。其函数定义如下:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/115_02.jpg?sign=1739162795-JK2zHrpLG3Y5msA7sLtutPPQ9XC6roY0-0-70d3650d8ef29e9fa804ab521d88913e)
利用递归,我们将fac()函数写成了一种非常紧凑的形式,如果n为1,返回1,否则返n*fac(n-1)。
递归可以更快地实现代码,不过在效率上可能会有一定的损失。
例如,斐波那契数列是这样的一个数列:1、1、2、3、5、8、13……其规律为:
F(0) = F(1) = 1, F(n) = F(n-1) + F(n-2)
即后一个数是前面两个数的和。
按照这个逻辑,我们很容易就能写出一个递归版本的代码:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/115_03.jpg?sign=1739162795-kGktaxjVFrq6p0ny6U7kaf5XfhQ27rJ5-0-9a8af42f3baf1d781e1338eaa0ed217d)
用非递归的方式的实现:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/115_04.jpg?sign=1739162795-Dz3rjJ1vENG4REs5fghzcqsSELwRgQTd-0-9a7267bb245be46ac2da83f83a5b5b03)
非递归的版本基本过程如下:
● 初始情况:a=F(0),b=F(1)
● 第一轮更新:a=F(1),b=F(0)+F(1)=F(2)
● 第二轮更新:a=F(2),b=F(3)
……
● 第n轮更新:a=F(n),b=F(n+1)
利用IPython的魔术命令%timeit,我们对这两个函数的运行时间进行比较:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/116_01.jpg?sign=1739162795-jWOkDpqU4iMVhCMkKteN2bVZMOTgnuV8-0-60e9c29c2b2d84146c6000d77bfe9ff7)
可以看到,两者的效率有很大的差别,非递归版本比递归版本要快很多。递归版本中存在大量的重复计算,例如,当我们调用fib1(n)时,需要计算一次fib1(n-1)和一次fib1(n-2),而调用fib1(n-1)时需要再调用一次fib1(n-2),这样fib1(n-2)事实上被计算了两次。
为了减少重复计算,可以考虑使用缓存机制来实现一个更快的递归版本,它利用默认参数可变的性质,使用缓存保存已经计算的结果:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/116_02.jpg?sign=1739162795-D6nqARHhY4oASQZqwMQ3932OMPWHXHmp-0-15f5c3a90be0dfea467f763b25bae385)
默认参数cache初始化为一个字典,并且存储初始值F(0)、F(1),它起到一个缓存的作用。计算fib(n)时,首先在缓存cache中查找,如果cache中有键n,直接返回结果;如果没有,抛出一个KeyError,在except的部分,函数使用递归更新cache[n]的值,然后返回它。
对于该函数,调用fib3(n-1)使得缓存中保存了cache[n-2],再调用fib3(n-2)的时候会直接返回结果而不是重复计算,提高了计算效率。
带缓存的递归版本的时间效率:
In [11]: %timeit fib2(20)
1000000 loops, best of 3: 230 ns per loop