不多说,直接上代码。
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 1
(虽然我知道这很煞风景,但是我真的忍不住想要吐槽一句,你这不就是把 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 语法呢。
|
7
stdout 2021-01-10 18:06:01 +08:00
很花哨实用。
|
8
python4 2021-01-10 18:06:18 +08:00 1
等 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 2021-01-10 19:09:25 +08:00 via iPhone
学习
|
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 2021-01-10 20:07:10 +08:00
`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 2021-01-11 19:44:23 +08:00
个人觉得可以写成装饰器
```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
abersheeran OP @yueyoum 喔。你是 Golang 的粉丝吧。
|
100
xuboying 2021-01-12 19:52:20 +08:00 via Android
@yueyoum 从语法的角度 op 的代码并不符合社区规范,但可能很多人(包括我)是把 op 的设计当成一个最终产品来用的,比如数据分析。op 更像是一个优秀的产品经理而不是单纯的 coder 。
|