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函数的用法示例:

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

现在我们用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 深入学习