函数式编程(Functional Programming)是一种编程范式,虽然不是本书重点阐述的内容,但 Python 语言很早就已经采用了一些函数式编程的概念,如1994年发布的 Python 版本中就已经有了 map()
、reduce()
、filter()
和 lambda
运算。之所以 Python 能支持函数式编程,是因为函数在 Python 中是第一类对象(参阅7.3.1节)。
本书不会深入介绍函数式编程(本节的命名显然有所夸大),如果对这种编程范式有兴趣,或者今后工作中会用到,建议自行阅读有关专业资料。下面只介绍与 Python 函数式编程有关的几个函数,这些函数在通常的程序中也较为常见。
Python 语言中用 def
定义的函数无一例外都有函数名称(也可以用于函数式编程),而用关键词 lambda
所创建的函数是一个匿名函数,可以把它看做是一个 lambda 表达式。其语法如下:
lambda <parameter_list>: <expression>
lambda
:关键词;<parameter_list>
:用逗号(英文)分割的参数(即形参);:
,英文状态下的冒号,分割参数与表达式;<expression>
:使用参数的表达式。例如,有这样的函数(用 def
创建):
>>> def add(x, y): return x + y
...
>>> add
<function add at 0x7fae31d44550>
>>> add(2, 3)
5
根据 lambda 表达式的语法,将函数 add()
改写为:
>>> lambda x, y: x + y
<function <lambda> at 0x7fae31d44430>
由此可知,由 lambda
关键词所创建的所谓表达式,实际上是一个函数对象,称为 lambda 函数,只不过由于它没有名字,我们不能像 add()
函数那样,使用其名称 add
来引用它。但是,既然它是对象,就可以通过赋值语句将它用变量引用。所以:
>>> add_lam = lambda x, y: x + y
>>> add_lam
<function <lambda> at 0x7fae31d44430>
现在变量 add_lam
引用了一个函数对象,如果调用该对象,与之前调用函数的方法则无异。
>>> add_lam(2, 3)
5
此外,也可以这样给 lambda 函数传实参。
>>> (lambda x, y: x + y)(2, 3)
5
显然,第一组圆括号内的就是 lambda 函数对象本身。
lambda 函数的最大特点就是以一个逻辑行代码创建了匿名函数。在某些应用中这样的代码会更简洁,应用得当可读性也很好。
例如判断 range(-5, 5)
中每个数是否大于 0
,用 lambda 函数可以写成:
>>> [(lambda x: x>0)(n) for n in range(-5, 5)]
[False, False, False, False, False, False, True, True, True, True]
如果这样写:
>>> [x > 0 for x in range(-5, 5)]
[False, False, False, False, False, False, True, True, True, True]
会显得更简洁,可读性更强。
所以,不要有了锤子,看任何东西都是钉子。
关于 lambda 函数,后续还会用到。
map()
是 Python 内置函数,它的基本调用格式是(来自于帮助文档):
map(func, *iterables) --> map object
func
引用一个函数对象;*iterables
收集多个可迭代对象。迭代器对象的成员依次作为 func
的实参。map object
是一个迭代器对象。例如7.4.1节中演示过的一个并不太好的写法:
>>> [(lambda x: x>0)(n) for n in range(-5, 5)]
[False, False, False, False, False, False, True, True, True, True]
用 map()
函数改写,会显得更紧凑简洁。
>>> m = map(lambda x: x > 0, range(-5, 5))
>>> m
<map object at 0x7fe1585559a0>
>>> list(m)
[False, False, False, False, False, False, True, True, True, True]
变量 m
引用的对象即为 map()
函数的返回值——迭代器对象(参阅第9章9.6节),通过 list(m)
可以查看迭代器对象的成员,与之前所得一样。类似的操作还可以有:
>>> r = map(lambda x: x**2, range(0, 20, 2))
>>> list(r)
[0, 4, 16, 36, 64, 100, 144, 196, 256, 324]
显然,上述结果也可以使用列表解析的方式得到:
>>> [i ** 2 for i in range(0, 20, 2)]
[0, 4, 16, 36, 64, 100, 144, 196, 256, 324]
很多时候,列表解析与 map()
函数可以互相替代,在实际应用时读者可以根据情况选择。
再举一个例子,假设有三个列表,lst1 = [1, 2, 3, 4, 5], lst2 = [6, 7, 8, 9, 0], lst3 = [7, 8, 9, 2, 1]
,将这三个列表中对应成员相加。根据前面的经验,至少可以有两种实现方式(请读者自行先尝试,再看下面的代码)。
>>> lst1 = [1, 2, 3, 4, 5]
>>> lst2 = [6, 7, 8, 9, 0]
>>> lst3 = [7, 8, 9, 2, 1]
# 列表解析
>>> [x + y + z for x, y, z in zip(lst1, lst2, lst3)]
[14, 17, 20, 15, 6]
# map() 函数
>>> r = map(lambda x, y, z : x + y + z, lst1, lst2, lst3)
>>> list(r)
[14, 17, 20, 15, 6]
这个示例中 map()
函数的 *iterables
参数收集了三个可迭代对象。
此外,参数 func
不一定总是 lambda 函数,任何函数对象均可以。
>>> def add(x):
... return x ** 2
...
>>> list(map(add, range(0, 10)))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
单词 filter 的中文含义是“过滤器”,在 Python 中,内置 fileter()
函数的作用也是如此,其帮助文档显示:
filter(function or None, iterable) --> filter object
第一个参数是函数对象或者 None
,第二个参数是可迭代对象。执行 filter()
函数,可迭代对象中的成员传给前面的函数对象,作为其实参。如果该函数返回的是 True
,那么这个成员就被放到一个名为“filter object”的迭代器中, filter()
函数最后返回的就是这个迭代器对象。
例如:
>>> f = filter(lambda x: x > 0, range(-5, 5))
>>> f
<filter object at 0x7fe15822a700>
>>> list(f)
[1, 2, 3, 4]
变量 f
引用的对象就是 filter()
函数返回的 filter 对象,通过 list()
转化之后看到所包含的成员,的确是根据所定义的 lambda 函数筛选之后的数值。
当然,时刻不要忘记,列表解析还是继续可用。
>>> [i for i in range(-5, 5) if i > 0]
[1, 2, 3, 4]
对于 filter()
的 function
的实参,除了 lambda 函数之外,也可以是任何函数对象。
>>> langs = ['python', 'PHP', 'Java', 'PER', 'Go']
>>> def all_caps(s):
... return s.isupper()
...
>>> list(filter(all_caps, langs))
['PHP', 'PER']
当然,上面的代码也可以用 lambda 函数改写,请读者自行尝试。
本节借用函数式编程的名义,介绍了 map()
和 filter()
两个内置函数以及 lambda 函数。从内容中读者也能认识到,这些函数均可以用以往学过的函数、列表解析等替代。所以,它们并非编程中的必须,只是可选项。
第3章3.3.1节曾简要介绍了与数学运算相关的 Python 内置函数,其实本节中的 map()
和 filter()
也是内置函数的一员。Python 内置函数所针对的通常是一些基础需求或问题,在自定义函数中,如果恰当使用内置函数,不仅能缩短代码行数,更能增强可读性,哪至于优化性能。例如写一个函数判断列表容器中的字符串成员是否有回文(关于回文,请参阅第4章4.2.5节),下面的函数 contains_palindrome()
是一种可行的方法:
#coding:utf-8
'''
filename: palindrome.py
'''
def contains_palindrome(words):
for word in words:
if word == ''.join(reversed(word)):
return True
return False
if __name__ == '__main__':
lst = ['why', 'your', 'eye', 'is', 'large']
print(contains_palindrome(lst))
程序的执行结果:
% python palindrome.py
True
字符串 lst
中的成员 'eye'
是回文,因此 contains_palindrome()
返回了 True
。下面要用内置函数 any()
改造 contains_palindrome()
函数内的代码。
def contains_palindrome_s(words):
return any(word == ''.join(reversed(word)) for word in words)
读者通过 help(any)
不难理解 any()
的作用。
>>> lst = ['why', 'your', 'eye', 'is', 'large']
>>> b = [word == ''.join(reversed(word)) for word in lst]
>>> b
[False, False, True, False, False]
>>> any(b)
True
[word == ''.join(reversed(word)) for word in lst]
以列表解析得到了一个用布尔值标识 lst
中的成员是否式回文的列表,在以此列表为 any()
的参数,则返回 True
。
但是,在函数 contains_palindrome_s()
中并没有使用列表解析,而是使用了第9章9.7节将要学习的生成器解析,其中道理请参阅该节内容。
与 any()
函数类似的另外一个内置函数是 all()
,留个读者探索使用它的时机。
除了这两个内置函数之外,其他内置函数也当然要在编程中恰当应用。