暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

在 Python 中实现“管道”

中航鲸技术 2021-10-26
2026

   


    在编码的时候,我们经常不是在处理数据,就是在处理数据的路上,当碰到多重嵌套循环数据的时候,我们就不得不编写一些嵌套循环的代码。这种情况下,我们应该非常渴望“管道”,来代替代码中的嵌套循环。

“管道” 是一种在两个进程间进行单向通信的机制。它可以将一个命令的结果传给另外一个命令的作为输入,让数据如同水管中的水流一样在命令间流动;数据流动的方向和人类思维方式保持一致,人类在看到管道连接的命令,就像在看一根水管,自然而然的将数据的流动想像成水的流动,从而减少大量组合计算,降低思考时的心智负担。




1
前置基础知识

   Python 没有在语言级别支持“管道”,所以我们可以自己来实现一下。在实现“管道”之前,我们需要来了解 2 个概念:

1. partial

   这个单词翻译成中文是“部分的”,在函数式编程里有重要的地位。它的功能就是给一个函数固定一些参数然后创建一个需要剩余参数的函数,著名的柯里化(currying)就是它的一种特例——柯里化只固定一个参数,偏函数允许你固定任意个参数。

   在 Python 标准库 functools
里有一个同名的函数 partial
就实现了相同的功能。看下面一段代码,通过 partial
这个函数,固定了 map
函数的 func
参数,并返回了一个新的函数 square
,它的功能是计算并返回每个元素的平方。

 from functools import partial

square = partial(map, lambda x: x**2)
square([1, 2, 3, 4])
# 等价于
map(lambda x: x**2, [1, 2, 3, 4])

print(list(square([1, 2, 3, 4]))) # [1, 4, 9, 16]


2. __ror__

    __ror__
是 Python 位运算 或( | )
的反向运算魔术方法,对于 Python 来说,两个数字的 按位
运算应该是这样的:

 a = 33; b = 44;
x = a | b
 print(x)  # 45

   当 a | b
时,Python 会优先调用 a.__or__(b)
方法,但是如果 a
没有实现 __or__
魔术方法,那么 Python 会反过来调用 b.__ror__(a)
方法,可以得到相同的结果。

 x = a | b
# 等价于
x = a.__or__(b)
# 等价于
x = b.__ror__(a)


2
实现管道

   了解完上面两个概念之后,我们来利用 partial
__ror__
来实现一个管道:

from functools import partial

class F(partial):
def __ror__(self, other):
return self(other)

   上面这段代码中,首先 F
继承了 partial
,这使得 F(map, lambda x: x*x)
的返回值是一个新的函数。与此同时 F
对象又被实现了 __ror__
这个魔术方法,这样一来就实现了 other | F(...)
这样的运算,不需要管 other
是否实现了 __or__/__ror__
魔术方法,因为我们只想可以像管道一样把 other 的结果传输给 F(...)
当作参数。 


3
使用样例

求 100 以内所有奇数之和:

 sum(filter(lambda x: x % 2, range(100)))
# 等价于
range(100) | F(filter, lambda x: x % 2) | F(sum)

下面再来一段实际应用:

 result = {}

for method in view.__methods__:
# 将请求方法转成全小写
method = method.lower()
# 过滤掉 “OPTIONS” 请求方法
if method == "options":
continue
# 生成该请求方法的文档
method_docs = self._generate_method(
getattr(view, method), path, definitions
)
# 过滤掉空文档
if not method_docs:
continue
result[method] = method_docs

由于这段 for
循环里的代码过多,无法使用推导式进行优化结构。看看完成相同的功能但使用“管道”的代码:

 generate_method_docs = lambda method: (
method,
_generate_method(getattr(view, method), path, definitions),
)

result = dict(
view.__methods__
| F(map, lambda method: method.lower())
| F(filter, lambda method: method != "options")
| F(map, generate_method_docs)
| F(filter, lambda method_docs: bool(method_docs[1]))
)

这样,由管道的每个函数完成自己的功能,避免了出现类似于 dict(filter(map(filter(map(...), ...), ...), ...))
这种多重嵌套的不友好情况。

   在两个示例中,管道的优点已经充分的体现出来了,管道操作使得数据的流向和阅读、思考代码的顺序达成一致,减轻了阅读者的心智负担。

   需要注意的是,这种操作在性能上略微有些的损耗,性能损耗并不只是管道造成的,大部分是由匿名函数的创建与销毁造成的;但是也不必太过担心,经过 100000 次循环的测试,管道操作的用时仅仅多出了 0.25 秒的用时,可以忽略不计了。


4
扩展

    F
有一个缺点,它只能把 |
左侧的值作为一个整体传递给被绑定的函数,不能传递多个参数。那么再实现一个 FF
,使用 Python 解参语法就可以自动的把可迭代对象变成多个参数传递给函数了。

 from functools import partial

class FF(partial):
def __ror__(self, other):
return self(*other)
def sum(n1, n2):
return n1 + n2


# 使用 FF 自动拆包
x = [18, 28] | FF(sum)
print(x) # 46


5
探讨更优雅的写法

    有人可能会提出 | F(...)
 这种写法很麻烦每次都需要写一个 F
能不能去掉但这个问题实际上是无法从运行时来解决的因为在运行时解决这个问题的前提是需要代码自己知道它后面是不是还跟着一个 |
 来进行管道运算以便于它判断是该返回一个类似于 F
 的对象还是真实的运算结果这是一个不可能在运行时完成的任务

    在 Python 社区也曾经出现过类似的库它的解决方案是 PIPE | range(100) | sum | END
通过 PIPE
 来创建一个可以反复进行 |
 运算的对象直到这个对象与 END
 这个对象进行了 |
 运算于是求值开始这便失去了 F
 的优点——你可以在任何一个管道运算之后停下来并放心的删掉后面所有的代码你拿到的依旧是真实运算的结果

    当然,最优雅的解决方案是把这件事放在编译期做在 Python 代码编译到 Python 字节码的时候判断 |
 并传,这样既不需要额外的运行时负担又可以做到管道的功能了


- END -

文章转载自中航鲸技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论