在用使用过 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 你是全程都不知道是个啥类型,这就是我放弃他的主要原因
有类型提示,很舒服
再举个例子,判断一个列表里是否有奇数。我们有这么个列表 [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,孰优孰劣一目了然。
说了这么多,怎么安装呢?
两种方式
- 从 pypi 安装
pip3 install superstream
然后导入即可使用
from superstream import Stream
- 直接复制源码使用 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 给我,谢谢。