Making Your Python Codes More Functional

407 查看

本篇文章是基于Joel Grus: Learning Data Science Using Functional Python视频的笔记。

常用的函数

currying

在Python中实现科里化的最佳方案是functools.partial。例如以下例子:

# 一般版本
def add1(x): return add(1, x)

# FP的版本
add1_functional = partial(add, 1)

reduce、map、filter

这几个是常见的FP中处理列表的函数,在此不做介绍。

注意:Python这得reducefunctools包中。

iterators(迭代器)

以下是个迭代器的例子:

In [4]: a  = [1,3,4]

In [5]: b = iter(a)

In [6]: next(b)
Out[6]: 1

In [7]: next(b)
Out[7]: 3

In [8]: next(b)
Out[8]: 4

In [9]: next(b)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-9-641a931447e8> in <module>()
----> 1 next(b)

StopIteration:

迭代器的特点主要包括:

  1. 一次只用一个

  2. 只有需要时才会产生数据。

这两个特点保证了其惰性的特点,而另一个好处是我们可以构造无穷序列

generator生成器

生成器所要生成的结果其实就是迭代器,如下:

def lazy_integers(n = 0):
    while True:
        yield n
        n += 1

xs = lazy_integers()

[next(xs) for _ in range(10)]
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

[next(xs) for _ in range(10)]
# [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

上面的例子我们可以看出,生成器在此生成了一个无穷长度的序列,由于其惰性我们可以在有限内存之中储存这个无穷序列。
但值得注意的是,因为迭代器中的每个值只能使用一次,就会得到同样语句不同结果的例子(见上)。

一个高阶的用法

squares = (x ** 2 for x in lazy_integers())
doubles = (x * x for x in lazy_integers())

next(squares) #0
next(squares) #1
next(squares) #4

我们发现使用tuple可以直接改变迭代器中的每个元素,这个特性很方便;但值得注意的是,不能写成列表的形式,不然输出值就不为惰性迭代器,就会引起内存外溢。

生成器模拟pipeline

考虑一个文件之中出现某个单词(例如“prime”)的句子个数,采用函数式的方法,显然,如下:

with open("a.txt", "r") as f:
    lines = (line for line in f)
    prime_lines = filter(lambda line: "prime" in line.lower(), lines)
    
    
line_count = len(list(prime_lines))

itertools模块

itertools模块提供了大量用于操作迭代器的函数。

函数名 参数 作用
count [start=0], [step=1] 输出无穷序列(start, start + step, start + 2 * step...)
islice seq, [start=0], stop, [step=1] 输出序列的切片
tee it, [n=2] 复制序列,输出为多个相同序列组成的元组
repeat elem, [n=forever] 重复序列n次,输出为一个repeat元素
cycle p 无限重复cycle里的元素
chain p, q, ... 迭代p里的元素,然后再迭代q的元素,...
accumulate p, [func=add] 返回(p[0], func(p[0], p[1]), func(func(p[0], p[1]), p[2])...)

自定义一些常用的迭代工具

def take(n, it):
    """
    将前n个元素固定转为列表
    """
    return [x for x in islice(it, n)]
    
    
def drop(n, it):
    """
    剔除前n个元素
    """
    return islice(it, n, None)
    

# 获取序列的头
head = next

# 获取除第一个元素外的尾
tail = partial(drop, 1) 

此外,很常见的另一个函数是获得一个递推的迭代器函数,即已知x, f,获得(x, f(x), f(f(x)),...)

def iterate(f, x):
    yield x
    yield from iterate(f, f(x))

注意,要防止mutation,就是说到底复制的是指针还是指针代表的数,显然下面的写法是有问题的:

def iterate(f, x):
    while True:
        yield x
        x = f(x)

一个简单的避免方法是:

def iterate(f, x):
    return accumulate(repeat(x), lambda fx, _:f(fx))

使iterate

def lazy_integers():
    return iterate(add1, 0)

take(10, lazy_integers())

一个例子:斐波那契数列

基本写法

def fib(n):
    if n == 0: return 1
    if n == 1: return 1
    return fib(n - 1) + fib(n - 2)


[fib(i) for i in range(10)]

升级写法——mutable but functional

def fibs():
    a, b = 0, 1
    while True:
        yield b
        a, b = b, a + b
        
take(10, fibs())

Haskell-Like 写法

def fibs():
    yield 1
    yield 1
    yield from map(add, fibs(), tail(fibs()))

take(10, fibs())

尾递归的haskell-like版本

def fibs():
    yield 1
    yield 1
    fibs1, fibs2 = tee(fibs())
    yield from map(add, fibs1, tail(fibs2))