Python函数及函数式编程
函数
函数调用
Python内置了很多有用的函数,我们可以直接调用。
要调用一个函数,需要知道函数的名称和参数,比如求绝对值的函数abs
,只有一个参数。可以直接从Python的官方网站查看文档:
http://docs.python.org/3/library/functions.html#abs
也可以在交互式命令行通过help(abs)
查看abs
函数的帮助信息。
调用abs
函数:
1 | >>> abs(100) |
数据类型转换
Python内置的常用函数还包括数据类型转换函数,比如int()
函数可以把其他数据类型转换为整数:
1 | >>> int('123') |
函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
1 | >>> a = abs # 变量a指向abs函数 |
函数调用
在Python中,定义一个函数要使用def
语句,依次写出函数名、括号、括号中的参数和冒号:
,然后,在缩进块中编写函数体,函数的返回值用return
语句返回。
我们以自定义一个求绝对值的my_abs
函数为例:
1 | def my_abs(nub): |
空函数
如果想定义一个什么事也不做的空函数,可以用pass
语句:
1 | def nop(): |
pass
语句什么都不做,那有什么用?实际上pass
可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass
,让代码能运行起来。
pass
还可以用在其他语句里,比如:
1 | if age >= 18: |
缺少了pass
,代码运行就会有语法错误。
参数检查
调用函数时,如果参数个数不对,Python解释器会自动检查出来,并抛出TypeError
:
1 | >>> my_abs(1, 2) |
但是如果参数类型不对,Python解释器就无法帮我们检查。试试my_abs
和内置函数abs
的差别:
1 | >>> my_abs('A') |
当传入了不恰当的参数时,内置函数abs
会检查出参数错误,而我们定义的my_abs
没有参数检查,会导致if
语句出错,出错信息和abs
不一样。所以,这个函数定义不够完善。
让我们修改一下my_abs
的定义,对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance()
实现:
1 | def my_abs(x): |
添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:
1 | >>> my_abs('A') |
返回多个值
函数可以返回多个值吗?答案是肯定的。
比如在游戏中经常需要从一个点移动到另一个点,给出坐标、位移和角度,就可以计算出新的新的坐标:
1 | import math |
import math
语句表示导入math
包,并允许后续代码引用math
包里的sin
、cos
等函数。
然后,我们就可以同时获得返回值:
1 | >>> x, y = move(100, 100, 60, math.pi / 6) |
但其实这只是一种假象,Python函数返回的仍然是单一值:
1 | >>> r = move(100, 100, 60, math.pi / 6) |
原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。
函数的参数
定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。
Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。
位置参数
对于power(x)
函数,参数x
就是一个位置参数。当我们调用power
函数时,必须传入有且仅有的一个参数x
。
如果我们要计算x^3怎么办?可以再定义一个power3
函数,但是如果要计算x^4、x^5……怎么办?我们不可能定义无限多个函数。
你也许想到了,可以把power(x)
修改为power(x, n)
,用来计算x^n。
修改后的power(x, n)
函数有两个参数:x
和n
,这两个参数都是位置参数,调用函数时,传入的两个值按照位置顺序依次赋给参数x
和n
。
1 | def power(x, n): |
默认参数
当有多个位置参数,且调用函数没有输入正确的参数时,就会报错。
比如:
1 | power(5) |
这个时候,我们就可以把n
设置为默认参数。
1 | def power(x, n=2): |
这样,当我们调用power(5)
时,相当于调用power(5, 2)
。
从上面的例子可以看出,默认参数可以简化函数的调用。设置默认参数时,有几点要注意:
一是必选参数在前,默认参数在后,否则Python的解释器会报错
二是如何设置默认参数。
当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
使用默认参数有什么好处?最大的好处是能降低调用函数的难度。
传入部分默认参数
如下一个函数,我们把年龄和城市设为默认参数:
1 | def enroll(name, gender, age=6, city='Beijing'): |
以下写法都是可以的:
enroll('Bob', 'M')
意思是,age
和city
都试用默认参数。enroll('Bob', 'M',7)
意思是,age
使用传入参数7,city
使用默认参数。enroll('Bob', 'M',city='ChengDu')
意思是,city
参数用传进去的值,其他默认参数继续使用默认值。
*默认参数必须指向不可变对象
先定义一个函数,传入一个list,添加一个END
再返回(错误示范):
1 | def add_end(L=[]): |
再次调用add_end()
时,结果就不对了:
1 | >>> add_end() |
默认参数是[]
,但是函数似乎每次都“记住了”上次添加了'END'
后的list。
原因解释如下:
Python函数在定义的时候,默认参数L
的值就被计算出来了,即[]
,因为默认参数L
也是一个变量,它指向对象[]
,每次调用该函数,如果改变了L
的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]
了。
** 定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None
这个不变对象来实现:
1 | def add_end(L=None): |
现在,无论调用多少次,都不会有问题:
1 | >>> add_end() |
为什么要设计str
、None
这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
可变参数
在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。
1 | def calc(*numbers): |
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*
号。在函数内部,参数numbers
接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
1 | >>> calc(1, 2) |
这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*
号,把list或tuple的元素变成可变参数传进去:
1 | >>> nums = [1, 2, 3] |
*nums
表示把nums
这个list的所有元素作为可变参数传进去。或者*numbers
表示把numbers
这个list的所有元素作为可变参数接收。这种写法相当有用,而且很常见。
关键字参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
1 | def person(name, age, **kw): |
可以传入任意个数的关键字参数:
1 | >>> person('Bob', 35, city='Beijing') |
关键字参数有什么用?它可以扩展函数的功能。比如,在person
函数里,我们保证能接收到name
和age
这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
1 | >>> extra = {'city': 'Beijing', 'job': 'Engineer'} |
当然,上面复杂的调用可以用简化的写法:
1 | >>> extra = {'city': 'Beijing', 'job': 'Engineer'} |
**extra
表示把extra
这个dict的所有key-value用关键字参数传入到函数的**kw
参数,kw
将获得一个dict,注意kw
获得的dict是extra
的一份拷贝,对kw
的改动不会影响到函数外的extra
。
其实,关键字参数和可变参数的逻辑是一样。只是关键字参数对应的dict,而可变参数对应的是list
命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw
检查。限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city
和job
作为关键字参数。
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city
和job
作为关键字参数。这种方式定义的函数如下:
1 | def person(name, age, *, city, job): |
和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。
调用方式如下:
1 | >>> person('Jack', 24, city='Beijing', job='Engineer') |
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
1 | def person(name, age, *args, city, job): |
参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:
1 | def f1(a, b, c=0, *args, **kw): |
最神奇的是通过一个tuple和dict,你也可以调用上述函数:
1 | >>> args = (1, 2, 3, 4) |
所以,对于任意函数,都可以通过类似func(*args, **kw)
的形式调用它,无论它的参数是如何定义的。
虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。
递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n
,用函数fact(n)
表示,可以看出:
fact(n) = n! = 1 x 2 x 3 x … x (n-1) x n = (n-1)! x n = fact(n-1) x n
所以,fact(n)
可以表示为n x fact(n-1)
,只有n=1时需要特殊处理。
于是,fact(n)
用递归的方式写出来就是:
1 | def fact(n): |
递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
高级特性
切片(Slice)
切片操作符是序列名后跟一个方括号,方括号中有一对可选的数字,并用冒号分割。注意这与你使用的索引操作符十分相似。记住数是可选的,而冒号是必须的。切片操作符中的第一个数(冒号之前)表示切片开始的位置,第二个数(冒号之后)表示切片到哪里结束,第三个数(冒号之后)表示切片间隔数。
如果不指定第一个数,Python就从序列首开始。如果没有指定第二个数,则Python会停止在序列尾。如果没有第三个数,则切片间隔数为1。
取一个list或tuple的部分元素是非常常见的操作。比如,一个list如下:
1 | >>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] |
取前3个元素,用一行代码就可以完成切片:
1 | >>> L[0:3] |
L[0:3]
表示,从索引0
开始取,直到索引3
为止,但不包括索引3
。即索引0
,1
,2
,正好是3个元素。
如果第一个索引是0
,还可以省略:
1 | >>> L[:3] |
同样的和索引操作一样,Python支持L[-1]
取倒数第一个元素,那么它同样支持倒数切片,试试:
1 | >>> L[-2:] |
tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:
1 | >>> (0, 1, 2, 3, 4, 5)[:3] |
字符串'xxx'
也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:
1 | >>> 'ABCDEFG'[:3] |
迭代(Iteration)
在Python中,迭代是通过for ... in
来完成的。Python的for
循环抽象程度要高于C的for
循环,因为Python的for
循环不仅可以用在list或tuple上,还可以作用在其他可迭代对象上。
list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:
1 | >>> d = {'a': 1, 'b': 2, 'c': 3} |
因为dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样。
默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values()
,如果要同时迭代key和value,可以用for k, v in d.items()
。
由于字符串也是可迭代对象,因此,也可以作用于for
循环:
1 | >>> for ch in 'ABC': |
所以,当我们使用for
循环时,只要作用于一个可迭代对象,for
循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。
那么,如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:
1 | >>> from collections import Iterable |
最后一个小问题,如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate
函数可以把一个list变成索引-元素对,这样就可以在for
循环中同时迭代索引和元素本身:
1 | >>> for i, value in enumerate(['A', 'B', 'C']): |
上面的for
循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码:
1 | >>> for x, y in [(1, 1), (2, 4), (3, 9)]: |
列表生成式(List Comprehensions)
列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。
它的基础语法是:[exp for iter_var in iterable]
首先迭代 iterable
里所有内容, 每一次迭代, 都把 iterable
里相应内容放到 iter_var
中, 再在表达式 `exp
中应用该 iter_var
的内容, 最后用表达式的计算值生成一个新的列表.
举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
可以用list(range(1, 11))
:
1 | >>> list(range(1, 11)) |
但如果要生成[1x1, 2x2, 3x3, ..., 10x10]
怎么做?
1 | >>> [x * x for x in range(1, 11)] |
for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:
1 | >>> [x * x for x in range(1, 11) if x % 2 == 0] |
运用列表生成式, 可以写出非常简洁的代码。
例如, 把一个 list 中所有的字符串变成小写:
1 | L = ['Hello', 'World', 'IBM', 'Apple'] |
还可以使用两层循环,可以生成全排列:
1 | >>> [m + n for m in 'ABC' for n in 'XYZ'] |
甚至是多层循环,但是一般只会使用2层:
1 | >>> [m + n + x +y for m in 'ABC' for n in 'XYZ' for x in 'XYZ'for y in 'XYZ'] |
for
循环其实可以同时使用两个甚至多个变量,比如dict
的items()
可以同时迭代key和value:
1 | >>> d = {'x': 'A', 'y': 'B', 'z': 'C' } |
再比如:L1 = ['Hello', 'World', 18, 'Apple', None]
,需求把列表中字符串大写修改为小写,非字符串原样。由于非字符串类型没有lower()
方法,所以列表生成式会报错。
1 | >>> L = ['Hello', 'World', 18, 'Apple', None] |
在
[ s.lower() if isinstance(s,str) else s for s in L]
中,可以理解成列表没有过滤条件,
[ s.lower() if isinstance(s,str) else s for s in L if True]
其中
s.lower() if isinstance(s,str) else s
是类似三目运算符的语法。(isinstance(s,str)?s.lower() :s
)语法结构为:
为真时的结果 if 判断条件 else 为假时的结果
(注意,没有冒号)
注意事项:
1. 当需要只是执行一个循环的时候尽量使用循环而不是列表解析, 这样更符合python提倡的直观性:
1
2for item in sequence:
process(item)122. 当有内建的操作或者类型能够以更直接的方式实现的, 不要使用列表解析.
例如复制一个列表时, 使用; L1=list(L) 即可, 不必使用: L1=[x for x in L]
3. 如果需要对每个元素都调用并且返回结果时, 应使用 L1=map(f,L), 而不是 L1=[f(x) for x in L].
个人觉得这是一个很有意思,很强大的特性。理解深入了对代码简洁易看有很大的帮助。
生成器(generator)
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]
改成()
,就创建了一个generator:
1 | >>> L = [x * x for x in range(10)] |
创建L
和g
的区别仅在于最外层的[]
和()
,L
是一个list,而g
是一个generator。
正确的方法是使用for
循环,因为generator也是可迭代对象:
1 | >>> g = (x * x for x in range(10)) |
yield
著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, …
斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:
1 | def fib(max): |
注意,赋值语句:
1 | a, b = b, a + b |
相当于:
1 | t = (b, a + b) # t是一个tuple |
但不必显式写出临时变量t就可以赋值。
上面的函数可以输出斐波那契数列的前N个数:
1 | >>> fib(6) |
仔细观察,可以看出,fib
函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。
也就是说,上面的函数和generator仅一步之遥。要把fib
函数变成generator,只需要把print(b)
改为yield b
就可以了:
1 | def fib(max): |
这就是定义generator的另一种方法。如果一个函数定义中包含yield
关键字,那么这个函数就不再是一个普通函数,而是一个generator:
1 | >>> f = fib(6) |
这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return
语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
举个简单的例子,定义一个generator,依次返回数字1,3,5:
1 | def odd(): |
调用该generator时,首先要生成一个generator对象,然后用next()
函数不断获得下一个返回值:
1 | >>> o = odd() |
可以看到,odd
不是普通函数,而是generator,在执行过程中,遇到yield
就中断,下次又继续执行。执行3次yield
后,已经没有yield
可以执行了,所以,第4次调用next(o)
就报错。
回到fib
的例子,我们在循环过程中不断调用yield
,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
同样的,把函数改成generator后,我们基本上从来不会用next()
来获取下一个返回值,而是直接使用for
循环来迭代:
1 | >>> for n in fib(6): |
但是用for
循环调用generator时,发现拿不到generator的return
语句的返回值。如果想要拿到返回值,必须捕获StopIteration
错误,返回值包含在StopIteration
的value
中:
1 | >>> g = fib(6) |
关于如何捕获错误,后面的错误处理还会详细讲解。
迭代器(Iterator)
我们已经知道,可以直接作用于for
循环的数据类型有以下几种:
一类是集合数据类型,如list
、tuple
、dict
、set
、str
等;
一类是generator
,包括生成器和带yield
的generator function。
这些可以直接作用于for
循环的对象统称为可迭代对象:Iterable
。
可以使用isinstance()
判断一个对象是否是Iterable
对象:
1 | >>> from collections import Iterable |
而生成器不但可以作用于for
循环,还可以被next()
函数不断调用并返回下一个值,直到最后抛出StopIteration
错误表示无法继续返回下一个值了。
可以被next()
函数调用并不断返回下一个值的对象称为迭代器:Iterator
。
可以使用isinstance()
判断一个对象是否是Iterator
对象:
1 | >>> from collections import Iterator |
生成器都是Iterator
对象,但list
、dict
、str
虽然是Iterable
,却不是Iterator
。
把list
、dict
、str
等Iterable
变成Iterator
可以使用iter()
函数:
1 | >>> isinstance(iter([]), Iterator) |
你可能会问,为什么list
、dict
、str
等数据类型不是Iterator
?
这是因为Python的Iterator
对象表示的是一个数据流,Iterator对象可以被next()
函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration
错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator
甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
函数式编程
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
高阶函数(Higher-order function)
变量可以指向函数
以Python内置的求绝对值的函数abs()
为例,调用该函数用以下代码:
1 | >>> abs(-10) |
但是,如果只写abs
呢?
1 | >>> abs |
可见,abs(-10)
是函数调用,而abs
是函数本身。
要获得函数调用结果,我们可以把结果赋值给变量:
1 | >>> x = abs(-10) |
但是,如果把函数本身赋值给变量呢?
1 | >>> f = abs |
结论:函数本身也可以赋值给变量,即:变量可以指向函数。
如果一个变量指向了一个函数,那么,可否通过该变量来调用这个函数?用代码验证一下:
1 | >>> f = abs |
成功!说明变量f
现在已经指向了abs
函数本身。直接调用abs()
函数和调用变量f()
完全相同。
函数名也是变量
那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()
这个函数,完全可以把函数名abs
看成变量,它指向一个可以计算绝对值的函数!
如果把abs
指向其他对象,会有什么情况发生?
1 | >>> abs = 10 |
把abs
指向10
后,就无法通过abs(-10)
调用该函数了!因为abs
这个变量已经不指向求绝对值函数而是指向一个整数10
!
当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。要恢复abs
函数,请重启Python交互环境。
注:由于abs
函数实际上是定义在import builtins
模块中的,所以要让修改abs
变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10
。
传入函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
1 | def add(x, y, f): |
当我们调用add(-5, 6, abs)
时,参数x
,y
和f
分别接收-5
,6
和abs
,根据函数定义,我们可以推导计算过程为:
1 | x = -5 |
map/reduce
map()
map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]
上,就可以用map()
实现如下:
1 | f(x) = x * x |
现在,我们用Python代码实现:
1 | >>> def f(x): |
map()
传入的第一个参数是f
,即函数对象本身。由于结果r
是一个Iterator
,Iterator
是惰性序列,因此通过list()
函数让它把整个序列都计算出来并返回一个list。
你可能会想,不需要map()
函数,写一个循环,也可以计算出结果:
1 | L = [] |
的确可以,但是,从上面的循环代码,能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗?
所以,map()
作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:
1 | >>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
只需要一行代码。
我们要数图书馆中的所有书。你数1号书架,我数2号书架。这就是“Map”。我们人越多,数书就更快。
reduce()
再看reduce
的用法。reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果就是:
1 | reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) |
比方说对一个序列求和,就可以用reduce
实现:
1 | >>> from functools import reduce |
当然求和运算可以直接用Python内建函数sum()
,没必要动用reduce
。
但是如果要把序列[1, 3, 5, 7, 9]
变换成整数13579
,reduce
就可以派上用场:
1 | >>> from functools import reduce |
我们要数图书馆中的所有书。你数1号书架,我数2号书架。现在我们到一起,把所有人的统计数加在一起。这就是“Reduce”。
filter()
Python内建的filter()
函数用于过滤序列。
和map()
类似,filter()
也接收一个函数和一个序列。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
例如,在一个list中,删掉偶数,只保留奇数,可以这么写:
1 | def is_odd(n): |
sorted()
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
Python内置的sorted()
函数就可以对list进行排序:
1 | >>> sorted([36, 5, -12, 9, -21]) |
此外,sorted()
函数也是一个高阶函数,它还可以接收一个key
函数来实现自定义的排序,例如按绝对值大小排序:
1 | >>> sorted([36, 5, -12, 9, -21], key=abs) |
key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs
处理过的list:
1 | list = [36, 5, -12, 9, -21] |
然后sorted()
函数按照keys进行排序,并按照对应关系返回list相应的元素:
1 | keys排序结果 => [5, 9, 12, 21, 36] |
我们再看一个字符串排序的例子:
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit']) |
默认情况下,对字符串排序,是按照ASCII的大小比较的,由于'Z' < 'a'
,结果,大写字母Z
会排在小写字母a
的前面。
现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
这样,我们给sorted
传入key函数,即可实现忽略大小写的排序:
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower) |
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True
:
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True) |
从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
匿名函数
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。
通过对比可以看出,匿名函数lambda x: x * x
实际上就是:
1 | def f(x): |
关键字lambda
表示匿名函数,冒号前面的x
表示函数参数。
匿名函数有个限制,就是只能有一个表达式,不用写return
,返回值就是该表达式的结果。
返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:
1 | def calc_sum(*args): |
但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
1 | def lazy_sum(*args): |
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数:
1 | >>> f = lazy_sum(1, 3, 5, 7, 9) |
调用函数f
时,才真正计算求和的结果:
1 | >>> f() |
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
1 | >>> f1 = lazy_sum(1, 3, 5, 7, 9) |
f1()
和f2()
的调用结果互不影响。
偏函数
Python的functools
模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。
int()
函数还提供额外的base
参数,默认值为10
。如果传入base
参数,就可以做N进制的转换:
1 | >>> int('12345', base=8) |
假设要转换大量的二进制字符串,每次都传入int(x, base=2)
非常麻烦,于是,我们想到,可以定义一个int2()
的函数,默认把base=2
传进去:
1 | def int2(x, base=2): |
这样,我们转换二进制就非常方便了:
1 | >>> int2('1000000') |
functools.partial
就是帮助我们创建一个偏函数的,不需要我们自己定义int2()
,可以直接使用下面的代码创建一个新的函数int2
:
1 | >>> import functools |
所以,简单总结functools.partial
的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
注意到上面的新的int2
函数,仅仅是把base
参数重新设定默认值为2
,但也可以在函数调用时传入其他值:
1 | >>> int2('1000000', base=10) |
最后,创建偏函数时,实际上可以接收函数对象、*args
和**kw
这3个参数,当传入:
1 | int2 = functools.partial(int, base=2) |
实际上固定了int()函数的关键字参数base
,也就是:
1 | int2('10010') |
相当于:
1 | kw = { 'base': 2 } |
当传入:
1 | max2 = functools.partial(max, 10) |
实际上会把10
作为*args
的一部分自动加到左边,也就是:
1 | max2(5, 6, 7) |
相当于:
1 | args = (10, 5, 6, 7) |
结果为10
。