函数是 Python 内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
而函数式编程(请注意多了一个“式”字)—— Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。
我们首先要搞明白计算机(Computer)和计算(Compute)的概念。
在计算机的层次上,CPU 执行的是加减乘除的指令代码
,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。
而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。
对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如 C 语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如 Lisp 语言。
?? 诞生 50 多年之后,函数式编程(functional programming)开始获得越来越多的关注。不仅最古老的函数式语言 Lisp 重获青春,而且新的函数式语言层出不穷,比如 Erlang、clojure、Scala、F# 等等。目前最当红的 Python、Ruby、Javascript,对函数式编程的支持都很强,就连老牌的面向对象的 Java、面向过程的 PHP,都忙不迭地加入对匿名函数的支持。越来越多的迹象表明,函数式编程已经不再是学术界的最爱,开始大踏步地在业界投入实用。也许继"面向对象编程"之后,"函数式编程"会成为下一个编程的主流范式(paradigm)。
4.1 什么是函数式编程
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
? 函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python 对函数式编程提供部分支持。由于 Python 允许使用变量,因此,Python 不是纯函数式编程语言。
4.1.1 定义
简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。
举例来说,现在有这样一个数学表达式:
(1 + 2) * 3 - 4
传统的过程式编程,可能这样写:
var a = 1 + 2;
var b = a * 3;
var c = b - 4;
函数式编程要求使用函数,我们可以把运算过程定义为不同的函数,然后写成下面这样:
var result = subtract(multiply(add(1,2), 3), 4);
这就是函数式编程。
4.1.2 特点
函数式编程具有五个鲜明的特点。
① 函数是"第一等公民"
所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
举例来说,下面代码中的 print 变量就是一个函数,可以作为另一个函数的参数。
var print = function(i){
console.log(i);
};
[1,2,3].forEach(print);
② 只用"表达式",不用"语句"
"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。
当然,实际应用中,不做 I/O 是不可能的。因此,编程过程中,函数式编程只要求把 I/O 限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。
③ 没有"副作用"
所谓"副作用")(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
④ 不修改状态
上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。
在其他类型的语言中,变量往往用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。下面的代码是一个将字符串逆序排列的函数,它演示了不同的参数如何决定了运算所处的"状态"。
function reverse(string) {
if(string.length == 0) {
return string;
} else {
return reverse(string.substring(1, string.length)) + string.substring(0, 1);
}
}
由于使用了递归,函数式语言的运行速度比较慢,这是它长期不能在业界推广的主要原因。
⑤ 引用透明
引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。
有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫"引用不透明",很不利于观察和理解程序的行为。
4.1.3 好处
函数式编程到底有什么好处,为什么会变得越来越流行?
① 代码简洁,开发快速
函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。
② 接近自然语言,易于理解
函数式编程的自由度很高,可以写出很接近自然语言的代码。
前文曾经将表达式 (1 + 2) * 3 - 4,写成函数式语言:
subtract(multiply(add(1,2), 3), 4)
对它进行变形,不难得到另一种写法:
add(1,2).multiply(3).subtract(4)
这基本就是自然语言的表达了。再看下面的代码,大家应该一眼就能明白它的意思吧:
merge([1,2],[3,4]).sort().search("2")
因此,函数式编程的代码更容易理解。
③ 更方便的代码管理
函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
④ 易于"并发编程"
函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。
请看下面的代码:
var s1 = Op1();
var s2 = Op2();
var s3 = concat(s1, s2);
由于 s1 和 s2 互不干扰,不会修改变量,谁先执行是无所谓的,所以可以放心地增加线程,把它们分配在两个线程上完成。其他类型的语言就做不到这一点,因为 s1 可能会修改系统状态,而 s2 可能会用到这些状态,所以必须保证 s2 在 s1 之后运行,自然也就不能部署到其他线程上了。
多核 CPU 是将来的潮流,所以函数式编程的这个特性非常重要。
⑤ 代码的热升级
函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。Erlang) 语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的。
下面进行具体函数具体示例介绍:
4.2 高阶函数
高阶函数英文叫 Higher-order function。什么是高阶函数?我们以实际代码为例子,一步一步深入概念。
(1)变量可以指向函数
以 Python 内置的求绝对值的函数abs()
为例,调用该函数用以下代码:
abs(-10) # 10
但是,如果只写abs
呢?


? 编写高阶函数,就是让函数的参数能够接收别的函数。
小结:
把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。
4.2.1 map/reduce
Python 内建了map()
和reduce()
函数。
如果你读过 Google 的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”,你就能大概明白 map/reduce 的概念。
① map
我们先看 map。map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
举例说明,比如我们有一个函数 $ f(x)=x^2$,要把这个函数作用在一个 list [1, 2, 3, 4, 5, 6, 7, 8, 9]
上,就可以用map()
实现如下:
![></cat_post_image><p>现在,我们用 Python 代码实现:</p><pre><code>def f(x):
return x * x
r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
list(r)</code></pre><cat_post_image><img src=](https://cos.gjcloak.xyz/images01/20200717105309.png)
首先,列出从2
开始的所有自然数,构造一个序列:
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
取序列的第一个数2
,它一定是素数,然后用2
把序列的2
的倍数筛掉:
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
取新序列的第一个数3
,它一定是素数,然后用3
把序列的3
的倍数筛掉:
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
取新序列的第一个数5
,然后用5
把序列的5
的倍数筛掉:
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
不断筛下去,就可以得到所有的素数。
用 Python 来实现这个算法,可以先构造一个从3
开始的奇数序列:
def _odd_iter():
n = 1
while True:
n = n + 2
yield n
注意这是一个生成器,并且是一个无限序列。
然后定义一个筛选函数:
def _not_divisible(n):
return lambda x: x % n > 0
最后,定义一个生成器,不断返回下一个素数:
def primes():
yield 2
it = _odd_iter() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(_not_divisible(n), it) # 构造新序列
这个生成器先返回第一个素数2
,然后,利用filter()
不断产生筛选后的新的序列。
由于primes()
也是一个无限序列,所以调用时需要设置一个退出循环的条件:
# 打印1000以内的素数:
for n in primes():
if n < 1000:
print(n)
else:
break
注意到Iterator
是惰性计算的序列,所以我们可以用 Python 表示“全体自然数”,“全体素数”这样的序列,而代码非常简洁。
练习题:
回数是指从左向右读和从右向左读都是一样的数,例如12321
,909
。请利用filter()
筛选出回数:
def is_palindrome(n):
return str(n) == str(n)[::-1]
测试:
# 测试:
output = filter(is_palindrome, range(1, 1000))
print('1~1000:', list(output))
if list(filter(is_palindrome, range(1, 200))) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191]:
print('测试成功!')
else:
print('测试失败!')
小结:
filter()
的作用是从一个序列中筛出符合条件的元素。由于filter()
使用了惰性计算,所以只有在取filter()
结果的时候,才会真正筛选并每次返回下一个筛出的元素。
4.2.3 sorted
① 排序算法
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个 dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
Python 内置的sorted()
函数就可以对 list 进行排序:
sorted([36, 5, -12, 9, -21])
![></cat_post_image><p>此外,<code>sorted()</code>函数也是一个高阶函数,它还可以接收一个<code>key</code>函数来实现自定义的排序,例如按绝对值大小排序:</p><pre><code>sorted([36, 5, -12, 9, -21], key=abs)</code></pre><cat_post_image><img src=](https://cos.gjcloak.xyz/images01/20200717175610.png)
此外,sorted()
函数也是一个高阶函数,它还可以接收一个key
函数来实现自定义的排序,例如按绝对值大小排序:
sorted([36, 5, -12, 9, -21], key=abs)
key 指定的函数将作用于 list 的每一个元素上,并根据 key 函数返回的结果进行排序。对比原始的 list 和经过key=abs
处理过的 list:
list = [36, 5, -12, 9, -21]
keys = [36, 5, 12, 9, 21]
然后sorted()
函数按照 keys 进行排序,并按照对应关系返回 list 相应的元素:
![></cat_post_image><p>我们再看一个字符串排序的例子:</p><pre><code>sorted(['bob', 'about', 'Zoo', 'Credit'])</code></pre><cat_post_image><img src=](https://cos.gjcloak.xyz/images01/20200717180026.png)
我们再看一个字符串排序的例子:
sorted(['bob', 'about', 'Zoo', 'Credit'])
默认情况下,对字符串排序,是按照 ASCII 的大小比较的,由于'Z' < 'a'
,结果,大写字母Z
会排在小写字母a
的前面。
现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个 key 函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
这样,我们给sorted
传入 key 函数,即可实现忽略大小写的排序:
sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
![></cat_post_image><p>要进行反向排序,不必改动 key 函数,可以传入第三个参数<code>reverse=True</code>:</p><pre><code> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)</code></pre><cat_post_image><img src=](https://cos.gjcloak.xyz/images01/20200717180306.png)
要进行反向排序,不必改动 key 函数,可以传入第三个参数reverse=True
:
sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
小结:
sorted()
也是一个高阶函数。用sorted()
排序的关键在于实现一个映射函数。
练习题:
假设我们用一组 tuple 表示学生名字和成绩:
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
请用sorted()
对上述列表分别按名字升序排序,再按成绩从高到低排序。
(1)按名字升序排序
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
def by_name(t):
return t[0]
L2 = sorted(L, key=by_name)
print(L2)
![></cat_post_image><p><strong>(2)按成绩从高到低排序</strong></p><pre><code>L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
def by_name(t):
return -t[1]
L2 = sorted(L, key=by_name)
print(L2)</code></pre><cat_post_image><img src=](https://cos.gjcloak.xyz/images01/20200717205003.png)

小结:
当函数的参数个数太多,需要简化时,使用functools.partial
可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。