Python函数式编程指南

Table of Contents

1 什么是函数式编程

  • 一种编程范式,之外还有面向对象(OOP)、面向过程、逻辑式编程
  • 把运算看作是数学中的函数。在一些语言中,没有运算符、操作符,一切都是函数,如Lisp:(+ 1 1)、(if (> 2 1) t nil)等等

2 函数式编程特点

  • 函数是一等公民,也就是可以作为高阶函数;
  • 引用透明,依赖参数输入,而不是外部变量;
  • 依赖函数返回值,一个参数对应唯一一个结果;
  • 避免副作用,副作用指会修改外部状态或依赖外部状态。

3 高阶函数

函数一等公民:函数跟其他类型的变量具有同等地位,可以被传来传去,也就说: 函数即可以作为函数的参数传递,也可以作为返回值返回。

4 不依赖或修改外部状态

“副作用”是指函数执行后会修改函数本身外的状态(比如全局变量),或者是因为外部变量的值从而影响到函数执行结果。比如下面一段依赖外部状态的代码:

is_find = False

def find():
    if xx:
	is_find = True

下面这段代码就不依赖外部变量:

def find():
    if xx:
	return True
    return False

4.1 只依赖参数

下面的代码,依赖外部:

conn = Connection()

def select():
    conn.find(..)

而下面的代码只依赖参数:

def select(conn):
    conn.find(..)

4.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

4.3 如何解决副作用和外部依赖?

  • 高阶函数,尽可能减少变量
  • 闭包函数,将依赖的状态闭包起来,形成一个独立的私有空间
  • 递归
  • 把核心代码写成函数式,和有副作用的代码隔离开

5 函数式编程的优点

因为函数式编程讲究的是无副作用,这种特性可以很好地完成一些工作,比如:

  • 并行/并发,不依赖外部状态,免去状态竞争;
  • 分布式计算,MapReduce作为代表;
  • 易测试;
  • 假如程序某个函数出现bug,在不中断程序运行的情况下去动态修正函数,这叫作“热补丁”,目前Lisp和Erlang支持。

6 函数式编程的语言

  • Haskell
  • Erlang
  • Clojure(函数式版Lisp方言,Lisp并不是函数式语言)
  • ……

不是只有函数式语言才能用函数式编程,越来越多的语言加入了函数式特性,如Python、Ruby、JavaScript、Java等。

7 高阶函数

示例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

示例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
    return reduce(operator.add, range(1, 101))

# for Lisp:
# Lisp中,+就是个函数
# (reduce + (range 1 101))

示例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

示例4,偏函数(partial)的应用:

import functools


def add(a, b):
    return a + b

add2 = functools.partial(add, 2)
print(add2(1))                  # => 3

8 惰性求值

指需要数据时才返回数据,惰性求值在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)

9 列表解析

源于Haskell,它的优点如下:

  • 用来高效创建列表或者遍历迭代器
  • 不用单独创建一个列表,避免副作用

示例:

[i ** 2 for i in range(10)]

10 Lambda表达式/匿名函数

匿名函数:没有函数名的函数对象

用途:在需要函数对象的地方实现一个简短的函数,但又没必要为此专门定义一个新函数

例:

number_list = range(10)
map(lambda n: n ** 2, number_list)

11 闭包

闭包可以在函数内部保存状态,例如Clojure中的memoize可让其他计算量大、较慢的函数具备“缓存”功能,重复调用可直接返回值,而不用再次计算。有了memoize,比如在对每行日志的IP标记地理位置,重复的IP没必要多次检索IP数据库。

12 综合实践,实现memoize

用Python实现Clojure中的memoize函数,实现前需要考虑:

1、如何给函数包一层有缓存功能的皮?

2、“缓存”的数据结构放哪儿?

知识点:闭包、高阶函数、一个参数对应唯一值。

完整代码如下:

import time

## for Clojure:
## user> (defn a [n] (Thread/sleep n) n)
## #'user/a
## user> (time (a 10))
## "Elapsed time: 10.27652 msecs"
## 10
## user> (time (a 1))
## "Elapsed time: 1.172558 msecs"
## 1
## user> (time (a 10))
## "Elapsed time: 10.213432 msecs"
## 10
## user> (def b (memoize a))
## #'user/b
## user> (time (b 10))
## "Elapsed time: 10.161806 msecs"
## 10
## user> (time (b 10))
## "Elapsed time: 0.086329 msecs"
## 10
## user> (time (b 10))
## "Elapsed time: 0.094688 msecs"
## 10

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))

13 深入学习