不多说,直接上代码。
from functools import partial class F(partial): def __ror__(self, other): if isinstance(other, tuple): return self(*other) return self(other)
使用样例。
range(10) | F(filter, lambda x: x % 2) | F(sum)
更详细的前因后果介绍,可以看 https://aber.sh/articles/Python-Pipe/ ,实在是懒得复制到 V2EX 调样式了。有兴趣的就看看,没兴趣的就复制代码去用就完事。
小小的得瑟一下,这是最近几天写的最满意、最有用的代码。妙手偶得之~
应某位回复者的要求,已经发布到 PyPi,顺便在 GitHub 建了个库,写了使用说明。https://github.com/abersheeran/only-pipe
稍微解决了一下好几个人觉得困扰的 tuple
判断。现在在库里,F 只会传一个参数,如果你想让 tuple 被拆解,应当使用 FF。是否解包,控制权交给程序员,而不是代码本身。
增加了一个功能,可以把 F
、reduce
释放到全局,就像 map
/filter
一样直接用就行。
import pipe import functools pipe.set_global(pipe.F, pipe.FF, functools.reduce) assert range(10) | F(filter, lambda x: x % 2) | F(sum) == 25 assert (1, 2, 3) | F(sum) == 6 assert (1, 2) | FF(lambda x, y: x + y) == 3 assert range(10) | F(reduce, lambda x, y: x + y) == 45
有回复者提到的 Filter = F(filter) 的类似写法,可通过手动柯里化实现。
from functools import reduce from pipe import F def fp(func): def _(*args, **kwargs): return F(func, *args, **kwargs) return _ Filter = fp(filter) Map = fp(map) Reduce = fp(reduce) range(100) | Filter(lambda x: x % 2) | Map(lambda x: x * x) | Reduce(lambda x, y: x + y)
![]() | 1 msg7086 2021-01-10 17:24:29 +08:00 ![]() (虽然我知道这很煞风景,但是我真的忍不住想要吐槽一句,你这不就是把 Python 写成了 Ruby 吗……) |
![]() | 2 msg7086 2021-01-10 17:27:20 +08:00 顺带提一句,如果让我来写 Python 的话,我可能还是选择把代码写成 list comprehension (可能需要推倒两次)。 不写 map reduce 是因为怕被 Python 用户吐槽不够 Pythonic 。 |
![]() | 3 abersheeran OP @msg7086 四年 Python 老玩家了,谁敢说我不代码 Pythonic 我就怼死他,我写 C 都 Python 味儿了。 Ruby 是真没用过,我甚至没有用过任何一门纯粹的函数式编程语言。管道我还是从 Shell 学到的,虽然 Shell 我只是用到 | grep 这种地步。但并不妨碍我真诚的觉得管道很好用。在我几个函数式教徒朋友给我推销的函数式编程概念里,柯里化和管道是我认为唯二可在工业代码里大量使用的。 |
![]() | 4 msg7086 2021-01-10 17:48:52 +08:00 我之前写任务依赖管理的时候也是把 pipe 拿来用了。 class Tasks < Array def |(op) op.call(sources: self) end end 类似这么个结构,感觉和你的差不多。 自己发明 DSL 确实很有成就感。 |
![]() | 5 Leigg 2021-01-10 17:54:14 +08:00 via iPhone 有半年没有写 py 了,现在都进化到这个程度了吗? |
![]() | 6 Leigg 2021-01-10 17:55:14 +08:00 via iPhone 我擦,原来是 magic method 。还以为是 shell 语法呢。 |
很花哨实用。 |
![]() | 8 python4 2021-01-10 18:06:18 +08:00 ![]() 等 python4 发布的时候就作为 feature 吧。 |
![]() | 9 SjwNo1 2021-01-10 18:20:54 +08:00 是很优雅,但不够语义化 - . - |
10 whileFalse 2021-01-10 18:41:38 +08:00 不错 很优雅! |
![]() | 11 abersheeran OP @python4 Guido:I don't like it. |
![]() | 12 ferock PRO 学习 |
13 Jirajine 2021-01-10 19:22:37 +08:00 via Android 不错,可以发布到 pypi |
![]() | 14 abersheeran OP ![]() |
![]() | 15 abersheeran OP @SjwNo1 如果你认为运算符不够语义化,建议开新文,炮轰 Shell 的管道运算符不够语义化。会有人跟你对线。 如果你认为 F 这个命名不够语义化可以用 Pipe,Python3 里你甚至可以给它一个中文名,管道。够语义化吗? |
16 Jirajine 2021-01-10 19:51:20 +08:00 via Android @abersheeran 复制粘贴是可以,但每个模块都复制粘贴一遍也不合适,还是得自己建一个模块再倒入,这和直接从 pypi 导入也没多大区别吧? 再者一般情况都从 pypi 找包,即使让人直接从 readme 里复制粘贴。不上的话不利于曝光需要的人也很难找到。 |
![]() | 17 YUX PRO `range(10) | F(filter, lambda x: x % 2) | F(sum)` 是奇数和叭 |
18 dorafmon 2021-01-10 20:54:25 +08:00 @Jirajine 用这个呗, 不是很接近了? https://pypi.org/project/pipetools/ |
![]() | 19 abersheeran OP @YUX 谢谢指正,写错了哈哈哈。好尴尬,待会就改。 |
![]() | 20 aijam 2021-01-10 21:12:58 +08:00 |
![]() | 21 abersheeran OP @Jirajine 有道理。待会就发一个。 |
![]() | 22 abersheeran OP @aijam 各有各的好。我这个一共只有六行代码,工作原理简单,对 Python 原本用法几乎无改变,可以和现在的代码混合使用,使用成本低、收益高。 你给的那个库,需要从数据源头包一层,返回的也是个它的自定义对象,没办法做到随意的混合使用。当然,它那个只需要在源头和结尾申明一下,可以少输入几个字符,这方面还是有优势的。 |
23 fpure 2021-01-11 00:18:19 +08:00 via Android 不错,很优雅的代码 |
![]() | 24 alan0liang 2021-01-11 08:18:57 +08:00 via Android 想到了 ECMAScript 的 proposal pipeline op: https://github.com/tc39/proposal-pipeline-operator 大概长这样: 64 |> Math.sqrt |> (x => x + 1) |> console.log 或者: 64 |> Math.sqrt |> # + 1 |> console.log |
![]() | 25 frostming 2021-01-11 08:42:12 +08:00 我记得很久之前哪里看到过,类似小技巧一类的文章 不过你这个可以柯里化一下更好: range(10) | F(filter)(lambda x: x % 2) | F(sum) |
![]() | 26 frostming 2021-01-11 08:42:44 +08:00 这样我可以: Filter = F(filter) Sum = F(sum) |
![]() | 27 abersheeran OP @frostming 嗯? F 从 partial 继承而来,本就可以绑定参数的。 |
![]() | 28 Ritter 2021-01-11 09:37:54 +08:00 一直以为 partial 是个函数 原来是个类。。。 |
29 zone10 2021-01-11 10:10:51 +08:00 我之前看过一个函数式的库, 也很有意思.我现在有个思路是用更高效的语言来实现这个,不知道能不能保证 Python 便利性的同时提高性能 |
![]() | 30 abersheeran OP @zone10 partial 本身就是 C 写的,虽然在标准库里也有一个 C 模块不可用时的 Pure Python 降级代替,但编译正常的 CPython 都会用那个 C 版本。所以这里的性能损耗几乎不可能更低了。 |
![]() | 31 abersheeran OP @Ritter 一般这种类似于一个函数功能的类,都会以小写命名,而不是死板的遵守 pep8 。 |
![]() | 32 SjwNo1 2021-01-11 10:27:15 +08:00 还有其他类似的实现吗,学习了 |
![]() | 33 Wincer 2021-01-11 10:34:20 +08:00 不错,不过 python 要是能在所有 function 共同的父类(说法不太准确?)上面定义 __ror__ 就好了,这样应该就可以实现:range(10) | filter(lambda x: x%2) 的写法,更接近于函数式编程语言的管道了。 |
![]() | 34 dinjufen 2021-01-11 10:58:16 +08:00 666,花哨的东西学不来 |
![]() | 35 frostming 2021-01-11 10:59:29 +08:00 |
![]() | 36 wellsc 2021-01-11 11:01:49 +08:00 666 这个好神奇 |
37 NeezerGu 2021-01-11 11:09:10 +08:00 @abersheeran 路过,昨天看这个学习了一阵,partial 是 python 代码实现的吧? |
![]() | 38 abersheeran OP @NeezerGu 自己看标准库源码吧。搜这一行 from _functools import partial |
![]() | 39 bruce00 2021-01-11 11:34:58 +08:00 楼主博客好看~ |
![]() | 40 Wincer 2021-01-11 11:37:54 +08:00 @frostming 沿用了你的代码,成功给 built-in 的 object 加上了 __ror__ 方法,但是还是行不通:range(10) | filter(lambda x: x % 2) 这个表达式被解释器会默认先求两端的值,如果不先求值的话,需要用 partial 包起来,不过这样又和楼主的实现差不多了。 |
![]() | 41 abersheeran OP @bruce00 谢谢~ |
![]() | 42 abersheeran OP @Wincer 要实现你这个,首先要让这些 callable 支持自动柯里化……不是支持一个管道运算符就行了的 自动柯里化的工作量就太大了,因为 Py 里存在 *args 和 **kwargs 。我是想不到有什么好办法能实现的。所以我选择让程序员自己柯里化一次(也就是使用 F 包裹一次)。 |
![]() | 43 Wincer 2021-01-11 11:46:59 +08:00 @abersheeran 嗯是的,这确实是目前比较合适的方法了 |
44 iqxd 2021-01-11 11:48:01 +08:00 请问下楼主 if isinstance(other, tuple): return self(*other) 是为了处理哪种情况呢? |
![]() | 45 abersheeran OP |
46 NeezerGu 2021-01-11 12:03:31 +08:00 @abersheeran 不是这个吗? https://github.com/python/cpython/blob/master/Lib/functools.py 哦,这是说, 同时提供了 python 版本和 c 版本? 如果 c 版本的导入失败就用 python 的? |
![]() | 47 frostming 2021-01-11 12:38:10 +08:00 @Wincer 如果只是 patch function 大可直接把 builtin 的 map, filter 换掉,我说的那种可以用来给 list 加 chaining call: [1, 2, 3].map(lambda x: x**2) |
![]() | 48 mckelvin 2021-01-11 13:01:55 +08:00 `sum(i for i in range(10) if i % 2)` 比 `range(10) | F(filter, lambda x: x % 2) | F(sum)` 更难读吗? |
49 yuruizhe 2021-01-11 13:04:14 +08:00 via iPhone @Leigg 同+1,我还纳闷 shell 咋能调用 python 函数呢…老实说,我还真没把 python shell 当成过主力 shell,都是 bash… |
![]() | 50 muzuiget 2021-01-11 13:10:53 +08:00 这种用法容易走火入魔。 |
![]() | 51 Merlini 2021-01-11 13:14:02 +08:00 之前做文本处理的时候看到网上一个 pipeline 写法感觉也挺不错。 ```python def pipeline( value: T, function_pipeline: Sequence[Callable[[T], T]], ) -> T: """A generic Unix-like pipeline :param value: the value you want to pass through a pipeline :param function_pipeline: an ordered list of functions that comprise your pipeline """ return reduce(lambda v, f: f(v), function_pipeline, value) ``` |
52 zouzou0208 2021-01-11 13:25:22 +08:00 好玩,原来是 index.py 的作者。好棒。 |
![]() | 53 chaleaoch 2021-01-11 13:29:09 +08:00 在这么玩, 会被玩坏的喂~~~~~ |
![]() | 54 shyling 2021-01-11 13:43:09 +08:00 f#自带语法 |
![]() | 56 shyrock 2021-01-11 14:42:38 +08:00 赞!学习了。 |
57 aldslvda 2021-01-11 14:52:48 +08:00 学习了 |
![]() | 58 iintothewind 2021-01-11 16:07:57 +08:00 你这个管道实现是不是可以再优化一下, 毕竟每次都得带 F() 包住后面的操作符, 这个看起来有点多余。 |
![]() | 59 admirez 2021-01-11 16:11:52 +08:00 pandas 也可以这样搞么? |
60 sapocaly 2021-01-11 16:50:00 +08:00 个人觉得 chain funciton call 可能更符合 python 一些至少更接近 django 一些。比如 range(10).filter(lambda x: x % 2).sum()。毕竟每次加个 F 还是有点难受。当然,去掉 F 容易,我是没想到怎么实现 range(10) | filter, lambada x:x |sum 这样的 syntax |
62 sapocaly 2021-01-11 17:46:39 +08:00 @frostming 不 我发现我之前的纠结没有意义, 如果只是要实现 range(10) | (filter, lambada x:x) |sum 或 range(10).filter(lambda x: x % 2).sum()这样的话,你只需开头用一个自己定一个 class,前者(pipe)照规矩 override ror 就行了,后者 chaining 的话我想用 metaclass 应该不难,不知道有没有更简单的。我觉得这种已经很 hack 了,改 buildin 就太过分了,没必要也不合适。当然如果你非要实现 range(10) | filter(lambada x:x) |sum 这样的,确实可能只能 patch 了,当然我会建议 patch a limited scope 而不是 explicitly override |
63 sapocaly 2021-01-11 17:52:14 +08:00 补充一下,比较容易实现的 chain 的用法会是 Chain.range(10).filter(lambda x:x % 2).sum().end(),稍微难一点的是 Chain.range(10).filter(lambda x:x % 2).sum(),我猜这里我得想下类似 lazy eval 的实现。如果想直接 range(10).filter(lambda x:x % 2).sum(),我可能会用 with patched_buildin(): range(10).filter(lambda x:x % 2).sum()这样的 syntax 当然讨厌 indentation 的话自然也有别的办法 |
64 sapocaly 2021-01-11 17:54:13 +08:00 想了想似乎 metaclass 都不需要 |
![]() | 65 abersheeran OP |
![]() | 66 xuboying 2021-01-11 18:04:45 +08:00 楼主不去写 perl6 可惜了,python 的名人名言是只能有一种写法,不遵守人的可以去开发其他语言(逃) |
![]() | 67 Tumblr 2021-01-11 18:07:05 +08:00 啊!这很 PowerShell !很 pwsh ! |
![]() | 68 abersheeran OP @mckelvin 我文章里解释过了。管道的数据处理顺序跟阅读顺序是一样的。利于阅读代码。 你说的这个嵌套用法当然可以,可是阅读顺序和实际的执行顺序是相反的,它先执行的内部函数再执行的外部函数。 |
69 NeezerGu 2021-01-11 18:12:21 +08:00 @abersheeran 原来如此,感谢哈。 |
70 sapocaly 2021-01-11 18:15:46 +08:00 @abersheeran 额其实我很久没写 python 了,倒不是真要用到,不过觉得你提出的这个问题挺有意思所以想想有啥别的实现。我看了那个库介绍,和我理想中的比较接近,我觉得 END 是可以去掉的,但当然这又要加很多 hack 。 至于哪种 syntax 我比较喜欢,我觉得都还行,不过我觉得我现实中不会去用。简单的逻辑没必要,复杂的逻辑 chain 或者 pipe 的可读性并不会增加,也大概率效率不是最好的。当然,我也很久没用 python 了,效率这个还得具体问题具体分析。 |
![]() | 71 xuboying 2021-01-11 18:31:51 +08:00 为何要特殊处理 tuple,没有 get 到 OP 的意图,有什么特殊情况可以简便写法么? >>> (1, 2 , 3) | F(filter, lambda x: x % 2) | F(list) TypeError: filter expected 2 arguments, got 4 >>> [1, 2 , 3] | F(filter, lambda x: x % 2) | F(list) [1, 3] >>> list(filter( lambda x: x % 2,(1,2,3))) [1, 3] |
![]() | 72 abersheeran OP @sapocaly 嗯,关于此的讨论你可以看看楼上我、 @frostming 和 @Wincer 发的。更好、更简单的实现方法目前来看,在 CPython 里是没有的。如果自己实现一个 Python 超集的编译器,那就可以做到了。 @iqxd @xuboying 因为我希望能传递多个参数给被 F 包裹的函数。一般来说,函数返回多值是 return x, y, z 这样,它的实际类型是一个 tuple 。 比如 def fa(...): return a, b, c def fb(a, b, c, d, e): return y 就可以直接 data | F(fa) | F(fb), 而不是 data | F(fa) | F(lambda args: F(fb(*args))) 这样写。 |
73 maddevil 2021-01-11 19:24:49 +08:00 from functools import partial F = type("F", (partial,), {"__ror__": lambda self, other: self(other)}) range(10) | F(filter, lambda x: x & 1) | F(sum) | F(print) |
![]() | 74 geebos PRO 个人觉得可以写成装饰器 ```python from functools import partial class F(partial): def __init__(self, func): self.__func = func def __ror__(self, other): if isinstance(other, tuple): return self(*other) return self(other) def __call__(self, *args, **kwargs): return self.__func(*args, **kwargs) def pipefy(func): return F(func) ``` |
75 zyb201314 2021-01-11 21:07:19 +08:00 via Android 这就是牛人技术探讨吗?作为小白的我完全参入不进讨论, 只能给你们鼓掌. |
76 crclz 2021-01-11 21:09:30 +08:00 可以可以,脚本语言就应该有脚本的样子 |
![]() | 77 24bit 2021-01-11 23:35:01 +08:00 赞一个 |
![]() | 78 lithbitren 2021-01-12 01:29:41 +08:00 非常有意思 |
79 AlexChing 2021-01-12 09:59:30 +08:00 这个在 python 的数据预处理阶段确实是很实用的。数据预处理需要按照特定的流程来走,自己一个个的写流程确实很难受。 |
![]() | 80 Reficul 2021-01-12 10:12:33 +08:00 差点真被唬住了, 原来是覆盖了位运算的或。。。 秒啊~ |
![]() | 81 xuboying 2021-01-12 10:56:15 +08:00 @abersheeran #72 感谢 OP 提供了一个非常好的思路,用在 notebook 里做数据分析非常实用,写项目估计会被大多数 team 打死。。。 ror 差点被骗了,以为是循环,看了一下文档以后我想再重载一个运算符,这样就不用粗暴的使用 tuple 来判断处理逻辑了,代码如下: $ cat foo.py from functools import partial class F(partial): def __ror__(self, rhs): return self(rhs) def __rrshift__(self, rhs): return self(*rhs) $ python -i foo.py >>> (1, 2 , 3) | F(filter, lambda x: x % 2) | F(list) [1, 3] >>> (14, '#b') >> F(format) '0b1110' >>> |
![]() | 83 abersheeran OP @xuboying 哈哈,如果你想要拆分成两个字符,不妨考虑 `|` 和 `^` 这两没有优先级问题。 |
84 Leviathann 2021-01-12 12:51:52 +08:00 via iPhone @alan0liang 这个 sharp 让我想到了 Mathematica |
![]() | 85 no1xsyzy 2021-01-12 13:25:00 +08:00 @abersheeran | ^ 这两个也有优先级问题, ^ 优先级高 似乎 | 没有同优先级的…… @geebos 你的 __init__ 和 __call__ 的功能已经被 partial 实现了…… |
![]() | 86 abersheeran OP |
![]() | 87 no1xsyzy 2021-01-12 13:48:22 +08:00 最长的管道实现:发 PEP 要求增加 |> 和 ||> 操作符 |
![]() | 88 no1xsyzy 2021-01-12 14:01:06 +08:00 @abersheeran 再想了下,可能比较歪 a / F(b) :: b(a) a // F(b) :: b(*a) a @ F(b) :: map(b, a) a % F(b) :: filter(b, a) a * F(b) :: reduce(b, a) |
![]() | 89 abersheeran OP @no1xsyzy 提这个 PEP,Guido 会回复:I don't like it. Closed. 哈哈哈 |
![]() | 90 woostundy 2021-01-12 15:30:06 +08:00 或运算有时候还挺常用的。 要是能自己增加运算符以及定义就好了。 |
![]() | 91 no1xsyzy 2021-01-12 15:32:04 +08:00 @abersheeran 大人,时代变了 Guido 不是也不喜欢 := 么…… |
![]() | 92 abersheeran OP @no1xsyzy 但是能和他对着干的,也就那么几个大佬吧。红姐提交模式匹配的 PR 不还是 Guido 直接 Closed 嘛。 |
![]() | 93 xuboying 2021-01-12 16:10:20 +08:00 @no1xsyzy #85 @abersheeran 花了点摸鱼的时间略微改造了一个符合我个人喜好的版本,以后做数据分析手指就轻松了。 我发现 partial 的类很特殊,不能简单继承,或者前面有人提到了 Filter=F(filter)似乎也是不行的。 希望有高人能帮忙简化一下我的写法。 $ cat foo.py from functools import partial class P(partial): """Pipe.""" def __ror__(self, rhs): return self(rhs) class X(partial): """Xargs.""" def __ror__(self, rhs): return self(*rhs) class Filter(P): def __new__(cls, *args, **kwargs): return super(__class__, cls).__new__(cls, filter, *args, **kwargs) class Map(P): def __new__(cls, *args, **kwargs): return super(__class__, cls).__new__(cls, map, *args, **kwargs) $ python -i foo.py >>> (1, 2 , 3) | Map(lambda x : x+2 ) | Filter(lambda x: x % 2) | X(divmod) | P (set) {0, 3} |
![]() | 94 abersheeran OP @xuboying https://github.com/abersheeran/only-pipe/discussions 方便的话可以发到这,留作记录。 你这个也不错。可以再加一个 Reduce 的,就齐活了哈哈哈。 |
![]() | 95 Macv1994 2021-01-12 17:11:58 +08:00 想问一下题主,怎么样才能像你一样写出这样优雅的代码,可以说一下学习路线吗? |
![]() | 96 abersheeran OP @Macv1994 学习路线?额……你可以看看我博客,按时间顺序倒着看。那个差不多就能展示我的学习路线了。 |
![]() | 97 yueyoum 2021-01-12 18:48:26 +08:00 我为了 回复此贴, 特意登陆了一下 总结: 哗众取宠,毫无意义 我已经 抛弃 python 了 工业应用,各种炫技代码,不是 平铺直述 的代码, 我全部都是 review 不通过的 如果一个 语言 写了 4 年 还能从 语法上玩出花样, 或者 一个语言 写了 4 年 还没掌握全部的语言特性 ( c++除外, 特定领域 和 高性能 是别人的卖点) 那么 在这种语言上 浪费时间是 毫无意义的 什么是好语言: 1. 语法 几天 学完 2. 语言特性 几个月 完全掌握 3. 有强力的 IDE 支持 4. 强类型,编译检查错误 5. 如果运行效率高最好 6. 如果容易的支持并行最好 python 一样都没占上 连最基本的 语言特性, 我敢说 这个帖子里 没有一个人 完全掌握了 python 的特性 所以, 自己玩玩可以 |
![]() | 98 OP @yueyoum 喔。你是 Golang 的粉丝吧。 |
![]() | 100 xuboying 2021-01-12 19:52:20 +08:00 via Android @yueyoum 从语法的角度 op 的代码并不符合社区规范,但可能很多人(包括我)是把 op 的设计当成一个最终产品来用的,比如数据分析。op 更像是一个优秀的产品经理而不是单纯的 coder 。 |