Python函数式编程系列012:惰性列表之生成器与迭代器

因为本系列还是基于一些已经对Python有一定熟悉度的读者,所以我们在此不做非常多的赘述来介绍基本知识了。而是回我们之前的主题,我们要用迭代器和生成器实现之前的指数函数。

当然,我们这里还是需要回到惰性列表是什么这个问题。事实上,回到原来惰性求值的概念,惰性列表的概念其实是「需要时才计算出值」的列表。我们在调用iter的时候,其实对常见的对象并没有特别大的优势。我们可以假想,其实iter转化[1, 2, 3, 4]的结果其实如下:

def yield_list():
    yield 1
    yield 2
    yield 3
    yield 4

唯一的优势,我们之前已经提到过了,就是反复套用函数fg时,我们是计算g(f(x))而不是先把列表里每个值套用f再套用g。这里有个极大的优势,就是提前终止时可以避免没有必要的运算。比如,下面一个for里面的例子,我们是为了发现列表ls中应用f函数后如果结果等于a就返回index否则返回None

def find_index_apply_f(f, ls, a):
    for i, x in enumerate(ls):
        if f(x) == a:
            return i
        else:
            continue
    return None

>>> find_index_apply_f(lambda x: x + 1, [1, 2, 3, 4, 5], 3)
1

现在,这里提前跳出可以减少非常多的运算量,但是如果使用一个普通列表却很难,我们在使用map之后必然已经全都计算了,但如果惰性求值,我们可以就在需要的时候停止就行。这个是列表操作替代循环必须实现的东西。

第二个惰性列表的最大应用,就是无穷列表,比如下面一个生成器,我们可以生成一个无限长度的全是x的列表。后面我们会聊到我们在各种场合中已经用到了这个抽象。

def yield_x_forever(x):
    while True:
        yield x

实现一些常用的(惰性)列表操作

大部分操作迭代器/生成器的函数,我们都可以在itertoools中找到。但,我们这里还是要实现一些非常函数式的函数,方便以后的操作:

1. head

head很简单,即取出(惰性)列表第一个元素:

head = next

2. take

take的目标是列表前N个值,这个可以实现成触发计算(转化成非惰性对象,一般为一个值或者列表)或者不触发计算的版本。下面我们实现的是触发计算的函数。

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

take_curry = lambda n: lambda it: take(n, it)

3. drop

drop则相反是删去前N个值。

def drop(n, it):
    """剔除前n个元素
    """
    return islice(it, n, None)

4. tail

tail是删去head后的列表,可以用drop实现:

from functools import partial

tail = partial(drop, 1)

5. iterate

iterate是重点要用到的函数,就是通过一个迭代函数还有初始值,实现一个无穷列表:

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

比如,实现所有正偶数的无穷列表:

positive_even_number = iterate(lambda x: x + 2, 2)

当然,更简单地写法是使用itertools里面的repeataccumulate

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

简单实践

例子一:求指数

我们回到之前求指数的例子中,我们可以实现惰性列表的版本。

第一个思路,我们就是直接用iteratex开始,每次乘以x,然后取出前n个值,拿到最后一个:

power = lambda x, n: take(n, iterate(lambda xx: xx * x, x))[-1]

另一个就是先生成一个无穷长度的x,取出前n个,相乘来reduce

power = lambda x, n: reduce(
    lambda x, y: x * y, 
    take(n, iterate(lambda _: x, x))
)

当然,我们还可以用生成器生成无穷长列表:

def yield_power(x, init=x):
    yield init
    yield from yield_power(x, init * x)

例子二:查找

我们回到上面解说的例子,我们要找到一个无穷列表中套用f后,第一个等于a的值的index。如果不是惰性的话,这个必须提前跳出也不可能实现。

def find_a_in_lazylist(f, lls, a):
    return head(filter(lambda x: f(x[1]) == a, enumerat(lls)))[0]

总结

本章回顾了利用Python自带的生成器、迭代器实现惰性列表,并展示如何运用这些概念做一些数据操作应用。当然在其中,我们要深刻感受到,函数式编程与数据是非常亲近的,它关注数据胜于项目结构,这点和对象式编程非常不同。大部分对象式编程的教程倾向于概述分层、结构这些概念,真是因为这个是对象式编程擅长的地方。

在我实现的教学项目fppy(点击这里前往github)中,我用内置的python模块实现了一个LazyList类,用它可以用链式写法完成上面的所有例子:

power1 = lambda x, n: LazyList.from_iter(x)(lambda xx: x * x).take(n).last
power2 = lambda x, n: LazyList.from_iter(x)(lambda _: x).take(n).reduce(lambda xx, yy: xx * yy)

find_a_in_lazylist = lambda f, lls, a: LazyList(lls)\
    .zip_with(LazyList.from_iter(0)(lambda x: x + 1))\
    .filter(lambda x: f(x[1]) == a)\
    .split_head()[0]

λ and τ
介绍关于数据可视化的方方面面,不光技术,还有哲学、文化、传播学。
1.2k 声望
101 粉丝
0 条评论
推荐阅读
基于Sanic的微服务基础架构
使用python做web开发面临的一个最大的问题就是性能,在解决C10K问题上显的有点吃力。有些异步框架Tornado、Twisted、Gevent 等就是为了解决性能问题。这些框架在性能上有些提升,但是也出现了各种古怪的问题难以...

jysong6阅读 4k评论 3

又一款眼前一亮的Linux终端工具!
今天给大家介绍一款最近发现的功能十分强大,颜值非常高的一款终端工具。这个神器我是在其他公众号文章上看到的,但他们都没把它的强大之处介绍明白,所以我自己体验一波后,再向大家分享自己的体验。

良许5阅读 1.8k

FastAPI性能碾压Flask?
不止一次的听过,FastAPI性能碾压Flask,直追Golang,不过一直没有测试过,今天闲着没事测试一下看看结果。不知道是哪里出了问题,结果大跌眼镜。

二毛erma02阅读 10.2k评论 3

封面图
Python之如何优雅的重试
为了避免偶尔的网络连接失败,需要加上重试机制,那么最简单的形式就是在对应的代码片段加一个循环,循环体里使用异常捕获,连接成功时退出循环,否则就重复执行相关逻辑,此时修改之后的函数f如下

Harpsichord12073阅读 7.3k

Linux终端居然也可以做文件浏览器?
大家好,我是良许。在抖音上做直播已经整整 5 个月了,我很自豪我一路坚持到了现在【笑脸】最近我在做直播的时候,也开始学习鱼皮大佬,直播写代码。当然我不懂 Java 后端,因此就写写自己擅长的 Shell 脚本。但...

良许1阅读 2.1k

基于 EKS Fargate 搭建微服务性能分析系统
近期 Amazon Fargate 在中国区正式落地,因 Fargate 使用 Serverless 架构,更加适合对性能要求不敏感的服务使用,Pyroscope 是一款基于 Golang 开发的应用程序性能分析工具,Pyroscope 的服务端为无状态服务且性...

亚马逊云开发者阅读 7.8k

Java 8 中需要知道的4个函数式接口-Function、Consumer、Supplier、Predicate
Java 8 中提供了许多函数式接口,包括Function、Consumer、Supplier、Predicate 等等。这 4 个接口就是本篇将要分享的内容,它们都位于 java.util.function 包下。

god23bin2阅读 423

封面图
1.2k 声望
101 粉丝
宣传栏