
在编码的时候,我们经常不是在处理数据,就是在处理数据的路上,当碰到多重嵌套循环数据的时候,我们就不得不编写一些嵌套循环的代码。这种情况下,我们应该非常渴望“管道”,来代替代码中的嵌套循环。
“管道” 是一种在两个进程间进行单向通信的机制。它可以将一个命令的结果传给另外一个命令的作为输入,让数据如同水管中的水流一样在命令间流动;数据流动的方向和人类思维方式保持一致,人类在看到管道连接的命令,就像在看一根水管,自然而然的将数据的流动想像成水的流动,从而减少大量组合计算,降低思考时的心智负担。
Python 没有在语言级别支持“管道”,所以我们可以自己来实现一下。在实现“管道”之前,我们需要来了解 2 个概念:
1. partial
这个单词翻译成中文是“部分的”,在函数式编程里有重要的地位。它的功能就是给一个函数固定一些参数然后创建一个需要剩余参数的函数,著名的柯里化(currying)就是它的一种特例——柯里化只固定一个参数,偏函数允许你固定任意个参数。
在 Python 标准库 functools
里有一个同名的函数 partial
就实现了相同的功能。看下面一段代码,通过 partial
这个函数,固定了 map
函数的 func
参数,并返回了一个新的函数 square
,它的功能是计算并返回每个元素的平方。
from functools import partialsquare = 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 | bprint(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)
了解完上面两个概念之后,我们来利用 partial
和 __ror__
来实现一个管道:
from functools import partialclass 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(...)
当作参数。
求 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:continueresult[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 秒的用时,可以忽略不计了。
F
有一个缺点,它只能把 |
左侧的值作为一个整体传递给被绑定的函数,不能传递多个参数。那么再实现一个 FF
,使用 Python 解参语法就可以自动的把可迭代对象变成多个参数传递给函数了。
from functools import partialclass 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
有人可能会提出 | F(...)
这种写法很麻烦F|
来进行管道运算F
的对象还是真实的运算结果
在 Python 社区也曾经出现过类似的库PIPE | range(100) | sum | ENDPIPE
来创建一个可以反复进行 |
运算的对象END
这个对象进行了 |
运算F
的优点
当然,最优雅的解决方案是把这件事放在编译期做|
并传参
- END -





