Python 函数式编程指南
Table of Contents
1. 什么是函数式编程
- 一种编程范式,之外还有面向对象(OOP)、面向过程、逻辑式编程
- 把运算看作是数学中的函数。在一些语言中,没有运算符、操作符,一切都是函数,如 Lisp:(+ 1 1)、(if (> 2 1) t nil) 等等
2. 函数式编程特点
- 函数是一等公民,可以作为“高阶函数”——把函数作为对象赋值到变量中,还可以把函数对象作为调用的参数以及返回值
- 引用透明,依赖参数输入,而不是外部变量
- 依赖函数返回值,一个参数对应唯一一个结果
- 避免副作用,副作用指会修改外部状态或依赖外部状态
- 递归作为主要的控制结构
2.1. 不依赖或修改外部状态
“副作用”是指函数执行后会修改函数本身外的状态(比如全局变量),或者是因为外部变量的值从而影响到函数执行结果。比如下面一段依赖外部状态的代码:
is_find = False def find(): if xx: is_find = True
下面这段代码就不依赖外部变量:
def find(): if xx: return True return False
2.1.1. 只依赖参数
下面的代码,依赖外部:
conn = Connection() def select(): conn.find(..)
而下面的代码只依赖参数:
def select(conn): conn.find(..)
2.1.2. 副作用
有副作用的代码,这段代码直接修改了参数值,并且影响到了外部:
def add_n(lst, n): for i in range(len(lst)): lst[i] += n return lst a = [1, 2, 3] print(add_n(a, 5)) # => [6, 7, 8] print(a) # => [6, 7, 8]
这段代码也有副作用,因为调用了 print,这会依赖外部 I/O 状态(因为“标准输出”可能被重定向):
def add_5(n): print(n) return n + 5
2.1.3. 如何解决副作用和外部依赖?
- 高阶函数,尽可能减少变量
- 闭包函数,将依赖的状态闭包起来,形成一个独立的私有空间
- 递归
- 把核心代码写成函数式,和有副作用的代码隔离开
2.2. 函数式编程的优点
因为函数式编程讲究的是无副作用,这种特性可以很好地完成一些工作,比如:
- 并行/并发,不依赖外部状态,免去状态竞争;
- 分布式计算,MapReduce 作为代表;
- 更简单的单元测试;
- 事件驱动,为事件传递一个函数对象作为事件触发时调用的回调函数;
- 热补丁,假如程序某个函数出现 bug,在不中断程序运行的情况下去动态修正函数。目前 Lisp 和 Erlang 支持。
2.3. 其他函数式编程的语言
- Haskell
- Erlang
- Clojure(函数式版Lisp方言,Lisp并不是函数式语言)
- ……
不是只有函数式语言才能用函数式编程,越来越多的语言加入了函数式特性,如 Python、Ruby、JavaScript、Java 等。
3. 高阶函数
函数一等公民:函数跟其他类型的变量具有同等地位,可以被传来传去,也就说: 函数即可以作为函数的参数传递,也可以作为返回值返回。
3.1. 示例1,map 函数的应用
示例代码如下:
def map_1(): number_list = range(10) return map(lambda n: n ** 2, number_list) # 同样的功能,用过程式实现如下: def map_2(): number_list = range(10) result = list() for i in number_list: result.append(i ** 2) return result
注意,在 Python2 中:
map(lambda i: i + 1, range(3))
会立即返回 [1, 2, 3]。而在 Python3 中,map 返回的是一个迭代对象,不会立即执行,要得到相同结果,需要显示调用 list:
list(map(lambda i: i + 1, range(3)))
3.2. 示例2,reduce 函数的应用
def _1_to_100(): """ 很多人这么写 """ n = 0 for i in range(1, 101): n = n + i return n def _1_to_100_1(): """ 函数式可以这么写 """ import operator from functools import reduce return reduce(operator.add, range(1, 101)) # for Lisp: # Lisp 中,+就是个函数 # (reduce + (range 1 101))
官方认为绝大多数应该用 for 循环,这样可读性更好,所以从 Python3 内置函数中移除了 reduce 函数,需要从 functools 库中引用:
import functools functools.reduce(lambda a, b: a + b, range(10))
3.3. 示例3,filter 函数的应用
def odd_p(n): """ 判断一个数是否是奇数 在 Lisp 中,这样的函数叫做谓词函数 """ if n % 2 != 0: return True return False # 找出 1~10 内的所有奇数 print(filter(odd_p, range(10))) # for Lisp: # (filter odd? (range 10)) # 过程式实现如下: def filter1(): number_list = range(10) result = list() for i in number_list: if odd_p(i): result.append(i) return result
3.4. 示例4,偏函数(partial)的应用
import functools def add(a, b): return a + b add2 = functools.partial(add, 2) print(add2(1)) # => 3
偏函数在日志中的应用示例:
import functools def debug(level, text): print('%s: %s' % (level, text)) info = functools.partial(debug, 'INFO') error = functools.partial(debug, 'ERROR')
3.5. 装饰器
把某个函数加上装饰器后,就可以为函数扩展额外的功能。本质上就是把函数作为参数传递的过程,如:
def show_call_func(func): print(func()) @show_call_func def say_hello(): return "hello world"
say_hello 函数加上 show_call_func 后,可以在调用完 say_hello 后,打印 say_hello 的返回值。
4. 惰性求值
指需要数据时才返回数据,惰性求值在 Python 可用迭代器(yield)完成:
# 1、不使用迭代器的情况,需要把文件所以行都存入列表中 # 如果文件行数太多,会占用大量内存 def test_1(): data = list() with open('test_data') as f: for line in f: data.append(line.strip()) for d in data: print(d) # 2、使用迭代器,需要一行的数据,就只返回一行,不需要全部加载到内存中 def test_2(): def read_data(): with open('test_data') as f: for line in f: yield line.strip() for d in read_data(): print(d)
5. 列表解析
源于 Haskell,它的优点如下:
- 用来高效创建列表或者遍历迭代器
- 不用单独创建一个列表,避免副作用
示例:
[i ** 2 for i in range(10)]
Python 还支持 Set 和 Dict 的列表解析:
# 字典的列表解析示例,生成 a~z 的 ASCII 码对应表 {chr(i):i for i in range(97, 123)} # set 的列表解析示例 {i for i in range(10)}
6. Lambda 表达式/匿名函数
匿名函数既没有函数名的函数对象,一般用在需要函数对象的地方实现一个简短的函数,但又没必要为此专门定义一个新函数,例如:
number_list = range(10) map(lambda n: n ** 2, number_list)
也可以把 lambda 赋值给一个变量:
n_square = lambda n: n ** 2 n_square(4) # => 16
7. 闭包
闭包可以在函数内部保存状态,例如 Clojure 中的 memoize 可让其他计算量大、较慢的函数具备“缓存”功能,重复调用可直接返回值,而不用再次计算。有了 memoize ,比如在对每行日志的 IP 标记地理位置,重复的 IP 没必要多次检索 IP 数据库。
8. 综合实践,实现 memoize
Clojure 中的 memoize 可以让某个函数具备缓存功能,如果调用该函数时传递了以前用过的参数,就可直接返回结果值,前提是函数一定是没有副作用的,memoize 在函数计算量比较大的情况下非常有用。memoize 函数的用法示例:
(defn a [n] (Thread/sleep n) n) (time (a 10)) ; "Elapsed time: 10.27652 msecs" ; => 10 (time (a 1)) ; "Elapsed time: 1.172558 msecs" ; => 1 (time (a 10)) ; "Elapsed time: 10.213432 msecs" ; => 10 (def b (memoize a)) (time (b 10)) ; "Elapsed time: 10.161806 msecs" ; => 10 (time (b 10)) ; "Elapsed time: 0.086329 msecs" ; => 10 (time (b 10)) ; "Elapsed time: 0.094688 msecs" ; => 10
现在我们用 Python 实现一个简易版本的,实现前需要考虑:
1、如何给函数包一层有缓存功能的皮?
2、“缓存”的数据结构放哪儿?
涉及的知识点有:闭包、高阶函数、函数无副作用。
完整代码如下:
import time def memoize(func): # 闭包,是为了保存状态 cache = dict() def _cache(args): if args in cache: return cache[args] cache[args] = apply(func, [args]) return cache[args] return _cache def test(n): # 这里有副作用的,其实,只是为了演示 time.sleep(n) return n def test2(n): time.sleep(n * 2) print(n) return n a = memoize(test) b = memoize(test2) print('a:', a(10)) print('a:', a(10)) print('b:', b(10)) print('b:', b(10))
9. 深入学习
- Functional Programming HOWTO:https://docs.python.org/2/howto/functional.html
- Introduction to Functional Programming:https://courses.edx.org/courses/course-v1:DelftX+FP101x+3T2015/info
- Functional Programming in Scheme:http://people.cs.aau.dk/~normark/prog3-03/html/notes/theme-index.html