SuperStream,一个 Python 轻量级类 Java Stream 的流式处理库

1,669 阅读6分钟

在用使用过 Typescript 与 Java 方便的链式调用后,回到 Python 再想实现同样的功能

高阶函数的套娃让你很痛苦吧?

性能开销都在其次,主要是写着蛋疼啊!

想想,要么高阶函数套娃,要么手撸 for 循环,经常还要写一些无用的中间变量,甚至常常没有了类型提示。特别是高阶函数套娃还是从里向外套,根本不符合常人的阅读习惯,或者甚至就是一大坨高阶函数 + lambda 函数 + 列表生成式的炫技产物,让你根本看了就头大,而按简单的写却要写很多行代码,失去了简洁性。让你每每写 Python 时就痛哭流涕,想要得到 Java/Typescript/Scala 等语言畅快的链式调用与类型提示,不用天天写出自己看了都痛苦的代码了

# 像这样,不知道你痛苦吗,反正我看着挺痛苦的
sum((pickle.load(open(path + p, 'rb')) for p in os.listdir(path) if p.startswith(starts)), [])

leetcode(832) 随便找个 leetcode 炫技题解,想要理解也是要一番功夫

所以我就在想,能不能 Python 也有一个流式处理库,于是我找到了 Pipetools,他能进行流式处理、惰性求值等计算功能,看起来就是我想要的库 github.com/0101/pipeto…

他给的例子也很有意思

假设你要输出文件夹下的所有文件名,并按文件名长度排列,并且给他标上序号

>>> print(pyfiles_by_length('../pipetools'))
1. ds_builder.py
2. __init__.py
3. compat.py
4. utils.py
5. main.py

一般人,可能会这么写

def pyfiles_by_length(directory):
    all_files = os.listdir(directory)
    py_files = [f for f in all_files if f.endswith('.py')]
    sorted_files = sorted(py_files, key=len, reverse=True)
    numbered = enumerate(py_files, 1)
    rows = ("{0}. {1}".format(i, f) for i, f in numbered)
    return '\n'.join(rows)

嗯,他能完成工作,但明显来了很多中间变量,那么高手可能会这么写

def pyfiles_by_length(directory):
    return '\n'.join('{0}. {1}'.format(*x) for x in enumerate(reversed(sorted(
        [f for f in os.listdir(directory) if f.endswith('.py')], key=len)), 1))

嗯,反正我已经看不懂了,可疯狂的科学家可能会这么写

pyfiles_by_length = lambda d: (reduce('{0}\n{1}'.format,
    map(lambda x: '%d. %s' % x, enumerate(reversed(sorted(
        filter(lambda f: f.endswith('.py'), os.listdir(d)), key=len))))))

反正我已经吐了,这谁看得懂啊

而 Pipetools 给我们了新的编写方式

pyfiles_by_length = (pipe
    | os.listdir
    | where(X.endswith('.py'))
    | sort_by(len).descending
    | (enumerate, X, 1)
    | foreach("{0}. {1}")
    | '\n'.join)

显然,代码清晰了很多,相比于普通人的写法,原始的数据一层层向下传递,最后得到了我们所要的产物,明显在可读性上提高了,更易于理解了,也更容易修改了,更解放了我们的心智,让我们少写了很多中间变量,让起名困难症 disappear 了

但是

在试用了 pipetools 后,我发现了一些问题

  • 上手难度高,我有点不知道怎么用
  • 没有类型提示,写着很痛苦

我放弃了,我想,我能不能自己搬一个 Java 的 Stream 库过来呢?

于是 SuperStream 就应运而生了,一个轻量级的类 Java Stream 库

他有什么好处?

  • 惰性求值,性能好
  • 代码可读性与简洁性兼顾,更容易写出优雅代码

来个简单的例子,有个二维列表[[1,2,3] [4,5,6] [7,8,9]],里面每个元素都是个列表,你要从里面筛出奇数然后每个数+1最后放到一个列表里,最后得到 [2,4,6,8,10]

痛苦的做法

foo = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
result = []
for inner_list in foo:
    for i in inner_list:
        if i % 2 == 1:
            result.append(i + 1)
print(result)  # [2, 4, 6, 8, 10]

要是用列表推导式,也得这样

print([i + 1 for inner_list in foo for i in inner_list if i % 2 == 1])  # [2, 4, 6, 8, 10]

看着是简洁了,可是可读性真的好吗?

如果你用 superstream,那么只需要

foo = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(Stream(foo)
      .flat_map(lambda x: Stream(x))
      .filter(lambda x: x % 2 == 1)
      .map(lambda x: x + 1)
      .to_list())  # [2, 4, 6, 8, 10]

哇,舒服了。兼顾了可读性与简洁性,数据就像管道一样传了下来。

最骚的是,他甚至可以自动类型推导,他是知道最后数据是 List[int] 类型的,而 pipetools 你是全程都不知道是个啥类型,这就是我放弃他的主要原因

image.png 有类型提示,很舒服

再举个例子,判断一个列表里是否有奇数。我们有这么个列表 [1,2,3,4]

普通人可能会这么写

foo = [1, 2, 3, 4]
flag = False
for i in foo:
    if i % 2 == 1:
        flag = True
        break

有经验的 python 程序员可能会这么写

foo = [1, 2, 3, 4]
flag = any(x % 2 for x in foo) # 别人看到:哇?这是什么黑魔法

但是问题来了,普通人的写法简单但是十分冗长,高手的写法又不是很易读,让我们来看看 stream 是怎么做的

foo = [1, 2, 3, 4]
flag = Stream(foo).any_match(lambda x: x % 2 == 1)

any_match 具有见文知意,lambda 函数也十分简洁易懂,兼具了简洁性与可读性

如果这还不能说服你,让我们来看一个业务上的例子

# 假设我们有一个服务,数据结构是

@dataclass
class ServiceInfo:
    name: str
    sre: str

# 有一个方法,从缓存里拿到服务信息,缓存里可能拿不到,拿不到时就返回空

def get_srv_info_from_cache(service: str) -> Optional[ServiceInfo]:
    pass

# 现在有一组服务
foo = ['com.xxx.foo', 'com.xxx.bar', 'com.xxx.baz']

# 我们需要过滤出 service name 为 com.xxx 开头的服务,拿到对应的 sre 集合
# 普通的做法可能会这么做 

sre_set = set()
for srv in foo:
    if srv.startswith('com.xxx'):
        srv_info = get_srv_info_from_cache(srv)
        if srv_info is not None:
            sre_set.add(srv_info.sre)

# 用了很多 for,if

# 而 Stream 的方式则像这样
sre_set = (Stream(foo)
           .filter(lambda x: x.startswith('com.xxx')) # 过滤出 com.xxx 开头的服务名
           .map(lambda x: get_srv_info_from_cache(x)) # 从缓存拿数据
           .filter(lambda x: x is not None) # 过滤掉没拿到的数据
           .map(lambda x: x.sre) # 获得 sre
           .to_set()) # 转集合

很显然,用 Stream 的方式优雅,每一步都很清晰,摆脱了传统方式大量的 for if。对于判空或者其他需要过滤的场景,只需要加一个 filter 即可过滤。试想如果我们刚刚的数据结构中,ServiceInfo 的 sre 若也可能为空,在传统方式又得加一个 if 判断,而 Stream 方式只需要再写一个 filter,孰优孰劣一目了然。

说了这么多,怎么安装呢?

两种方式

  1. 从 pypi 安装
pip3 install superstream

然后导入即可使用

from superstream import Stream 
  1. 直接复制源码使用 github.com/Shimada666/… 复制 src/superstream/init.py 的内容到自己项目下,手动引用即可

目前已支持

  • map
  • foreach
  • filter
  • reduce
  • sorted
  • limit
  • skip
  • count
  • distinct
  • flatmap
  • findfirst
  • anymatch/allmatch/nonematch

后续还将支持一些新特性,如 groupby, joining 等。如果实现方便,可能会支持并行流 parallel stream。

结语

希望这个库能帮到有需要的人,如果有什么好的意见或建议,也欢迎提 issue、pr 给我,谢谢。