Python中类似于R的magrittr包中的%>%的函数管道

135

在R语言中(感谢magrittr),您现在可以通过%>%使用更加函数式的管道语法执行操作。这意味着,您不需要编写以下代码:

> as.Date("2014-01-01")
> as.character((sqrt(12)^2)

你也可以这样做:

> "2014-01-01" %>% as.Date 
> 12 %>% sqrt %>% .^2 %>% as.character

对我来说,这种方式更易读,并且适用于数据框之外的用例。 Python语言是否支持类似的功能呢?


2
很好的问题。我特别关注函数有更多参数的情况。就像在 How dplyr replaced my most common R idioms 的结尾处所示,crime_by_state %>% filter(State=="New York", Year==2005) ... - Piotr Migdal
1
当然,一个人可以使用许多lambda、map和reduce来完成它(而且这样做很简单),但简洁和可读性是主要的关键点。 - Piotr Migdal
14
涉及的软件包是magrittr。 - piccolbo
1
是的,出于同样的原因,每个编写的R包都是由Hadley撰写的。他更加知名。(这里有一点嫉妒) - piccolbo
1
查看解决此问题的 https://dev59.com/3FwX5IYBdhLWcg3wjwJZ 的答案。 - Piotr Migdal
显示剩余3条评论
16个回答

59
管道是 Pandas 0.16.2 的新功能。
示例:
import pandas as pd
from sklearn.datasets import load_iris

x = load_iris()
x = pd.DataFrame(x.data, columns=x.feature_names)

def remove_units(df):
    df.columns = pd.Index(map(lambda x: x.replace(" (cm)", ""), df.columns))
    return df

def length_times_width(df):
    df['sepal length*width'] = df['sepal length'] * df['sepal width']
    df['petal length*width'] = df['petal length'] * df['petal width']
    
x.pipe(remove_units).pipe(length_times_width)
x

注意:Pandas版本保留Python的引用语义。这就是为什么length_times_width不需要返回值;它直接修改了x

11
不幸的是,这仅适用于数据框,因此我不能将其指定为正确答案。但是很好地提到了这里,因为我考虑的主要用例是将其应用于数据框。 - cantdutchthis

48

使用一个名为macropy的模块是实现这一点的一种可能方法。Macropy允许您对编写的代码应用转换。因此,a | b可以转换为b(a)。这有许多优点和缺点。

与Sylvain Leroux提到的解决方案相比,主要优点在于您不需要为您想要使用的函数创建中缀对象 - 只需标记您打算使用转换的代码区域即可。其次,由于转换是在编译时应用而不是运行时应用,因此转换后的代码在运行时不会受到任何开销 - 所有工作都在字节码首次从源代码生成时完成。

主要劣势是macropy需要以特定方式激活才能工作(稍后提到)。与更快的运行时相比,源代码的解析更加计算复杂,因此程序启动需要更长时间。最后,它添加了一种语法风格,意味着不熟悉macropy的程序员可能会发现您的代码难以理解。

示例代码:

run.py

import macropy.activate 
# Activates macropy, modules using macropy cannot be imported before this statement
# in the program.
import target
# import the module using macropy

target.py

from fpipe import macros, fpipe
from macropy.quick_lambda import macros, f
# The `from module import macros, ...` must be used for macropy to know which 
# macros it should apply to your code.
# Here two macros have been imported `fpipe`, which does what you want
# and `f` which provides a quicker way to write lambdas.

from math import sqrt

# Using the fpipe macro in a single expression.
# The code between the square braces is interpreted as - str(sqrt(12))
print fpipe[12 | sqrt | str] # prints 3.46410161514

# using a decorator
# All code within the function is examined for `x | y` constructs.
x = 1 # global variable
@fpipe
def sum_range_then_square():
    "expected value (1 + 2 + 3)**2 -> 36"
    y = 4 # local variable
    return range(x, y) | sum | f[_**2]
    # `f[_**2]` is macropy syntax for -- `lambda x: x**2`, which would also work here

print sum_range_then_square() # prints 36

# using a with block.
# same as a decorator, but for limited blocks.
with fpipe:
    print range(4) | sum # prints 6
    print 'a b c' | f[_.split()] # prints ['a', 'b', 'c']

最后是执行重要工作的模块。我将其称为fpipe,代表函数管道,因为它模拟了Shell语法,用于将一个进程的输出传递到另一个进程。

fpipe.py

from macropy.core.macros import *
from macropy.core.quotes import macros, q, ast

macros = Macros()

@macros.decorator
@macros.block
@macros.expr
def fpipe(tree, **kw):

    @Walker
    def pipe_search(tree, stop, **kw):
        """Search code for bitwise or operators and transform `a | b` to `b(a)`."""
        if isinstance(tree, BinOp) and isinstance(tree.op, BitOr):
            operand = tree.left
            function = tree.right
            newtree = q[ast[function](ast[operand])]
            return newtree

    return pipe_search.recurse(tree)

3
听起来很不错,但据我所知只能在Python 2.7上运行(而不能在Python 3.4上运行)。 - Piotr Migdal
4
我已经创建了一个没有依赖关系的较小库,它与@fpipe装饰器执行的相同操作,但是重新定义了右移运算符(>>)而不是或运算符(|):https://pypi.org/project/pipeop/。 - Robin Hilliard
被踩是因为需要使用第三方库和多个装饰器来解决一个相对简单的问题,这是非常复杂的解决方案。此外,它只适用于Python 2。我相信原生Python的解决方案也会更快。 - jramm

34

1
PyToolz 是一个很好的指针。话虽如此,其中一个链接已经失效,另一个链接也即将失效。 - akhmed
2
他的基本URL似乎是:http://matthewrocklin.com/blog/和PyToolz http://toolz.readthedocs.io/en/latest/。啊,互联网的短暂性... - smci
这个让人不爽的地方是你不能使用多参数函数。 - Frank
2
@Frank:嘿,这是开源的,作者不会从你或我这里得到报酬,所以不要说“X包烂透了”,而是说“X包只适用于Y情况”,并且建议更好的替代包,或者为X包贡献该功能,或者自己动手写。 - smci
sspipe非常好用。另外,我并没有说这个软件包很糟糕,我是说缺少一些功能很糟糕。 - Frank

25

如果您只是想用于个人脚本编写,您可能需要考虑使用Coconut而不是Python。

Coconut是Python的超集。因此,您可以使用Coconut的管道运算符|>,同时完全忽略Coconut语言的其他部分。

例如:

def addone(x):
    x + 1

3 |> addone

编译成

# lots of auto-generated header junk

# Compiled Coconut: -----------------------------------------------------------

def addone(x):
    return x + 1

(addone)(3)

print(1 |> isinstance(int)) TypeError:isinstance 需要2个参数,但只提供了1个 - nyanpasu64
2
如果您仍然遇到这个问题,请尝试 print(1 |> isinstance$(int)),或者更好的方法是 1 |> isinstance$(int) |> print - Solomon Ucko
@Solomon Ucko,你的答案是错误的。1 |> print$(2) 调用的是 print(2, 1),因为 $ 映射到 Python 的 partials。但我想要 print(1, 2),这与 UFCS 和 magrittr 相匹配。动机:1 |> add(2) |> divide(6) 应该是 0.5,而且我不应该需要括号。 - nyanpasu64
看到 |>(+)$(?, 2) 都有多么丑陋,我得出结论:编程语言和数学界不希望我使用这种类型的语法,甚至比使用一组括号还要难看。如果它有更好的语法(例如 Dlang 有 UFCS,但我不知道算术函数如何,或者 Python 是否有 .. 管道运算符),我会使用它的。 - nyanpasu64
没有调试器和有限的IDE支持(例如重构):致命问题。我会坚持使用像_fluentpy_这样的东西。 - WestCoastProjects
显示剩余5条评论

23

Python语言是否支持类似的功能?

"更具函数式特色的管道语法",这真的是更具"函数式"语法吗?我会说这只是为R添加了一个"infix"语法。

话虽如此,Python的文法并没有直接支持除标准运算符外的中缀表达式。


如果你真的需要这样的东西,你可以参考Tomer Filiba的代码,作为实现自己中缀表达式的起点:

Tomer Filiba提供的代码示例和注释(来源:http://tomerfiliba.com/blog/Infix-Operators/):

from functools import partial

class Infix(object):
    def __init__(self, func):
        self.func = func
    def __or__(self, other):
        return self.func(other)
    def __ror__(self, other):
        return Infix(partial(self.func, other))
    def __call__(self, v1, v2):
        return self.func(v1, v2)
使用这个特殊类的实例,我们现在可以使用一种新的"语法"来调用函数作为中缀运算符:
>>> @Infix
... def add(x, y):
...     return x + y
...
>>> 5 |add| 6

18

有一个叫做dfply的模块。您可以在https://github.com/kieferk/dfply找到更多信息。

以下是一些示例:

from dfply import *
diamonds >> group_by('cut') >> row_slice(5)
diamonds >> distinct(X.color)
diamonds >> filter_by(X.cut == 'Ideal', X.color == 'E', X.table < 55, X.price < 500)
diamonds >> mutate(x_plus_y=X.x + X.y, y_div_z=(X.y / X.z)) >> select(columns_from('x')) >> head(3)

在我看来,这应该被标记为正确答案。此外,似乎 dfplydplython 都是相同的软件包。它们之间有什么区别吗?@BigDataScientist - InfiniteFlash
dfplydplythonplydata 包是 dplyr 包的 Python 移植版,因此它们在语法上非常相似。 - BigDataScientist
dfply 是最近唯一被稍微更新的一个:即使在2021年3月,它也没有关闭的问题或提交记录。我联系了该项目,想知道他们是否有任何计划“苏醒”。 - WestCoastProjects
顺便提一下,我还维护另一个名为siuba的端口。它具有生成SQL代码和加速分组操作的额外优势!https://github.com/machow/siuba - machow
绝对不是正确的答案。3 >> np.sqrt会出错,但在R中是3 %>% sqrt - Frank
@Frank,Python 中无法使用 %>%,因此这可能是最接近的 Python 等效语句。 - WestCoastProjects

17
你可以使用sspipe库。它公开了两个对象ppx。类似于x %>% f(y,z),你可以写成x | p(f, y, z),类似于x %>% .^2,你可以写成x | px**2
from sspipe import p, px
from math import sqrt

12 | p(sqrt) | px ** 2 | p(str)

这看起来不错,但你能将其管道传递到第二个变量 3 | p( f( x, . ) ) 吗?在 R 中,这将是:3 %>% f(x, .) - Frank
1
@Frank 是的。这可以通过使用 px 实现,它类似于 R 中的 .。但是,你应该注意将 px 传递给 p() 而不是 f()。例如:3 | p(f, x, px) - mhsekhavat
1
在应用程序中,这个答案似乎是最接近 dplyr %>% 的。 - Jake

12

我很想在Python中使用Elixir的管道运算符|>,所以我创建了一个简单的函数修饰器(约50行代码),它使用ast库和编译/执行将Python的右移运算符>>重新解释为非常类似Elixir的管道操作符。

from pipeop import pipes

def add3(a, b, c):
    return a + b + c

def times(a, b):
    return a * b

@pipes
def calc()
    print 1 >> add3(2, 3) >> times(4)  # prints 24

它所做的只是将a >> b(...)重写为b(a, ...)

https://pypi.org/project/pipeop/

https://github.com/robinhilliard/pipes


12

实现管道功能无需第三方库或混淆的操作符技巧-您可以很容易地自己实现基础功能。

首先,让我们定义一下管道函数到底是什么。其核心只是一种以逻辑顺序表达连续函数调用的方式,而不是标准的“从里到外”的顺序。

例如,让我们看看这些函数:

def one(value):
  return value

def two(value):
  return 2*value

def three(value):
  return 3*value

虽然不是很有趣,但假设有一些有趣的事情正在发生在value上。我们希望依次调用它们,并将每个输出传递给下一个。在普通的Python中,代码如下:

result = three(two(one(1)))

这段代码不太易读,而且在处理更复杂的管道时会变得更加糟糕。因此,这里有一个简单的 pipe 函数,它接受一个初始参数和一系列要应用于该参数的函数:

def pipe(first, *args):
  for fn in args:
    first = fn(first)
  return first

我们称其为:

result = pipe(1, one, two, three)

对我来说,那看起来像是非常易读的“管道”语法 :). 我不认为它比重载运算符或类似的语法不易读。实际上,我认为它比python代码更易读。

这里是简单的管道解决OP的示例:

from math import sqrt
from datetime import datetime

def as_date(s):
  return datetime.strptime(s, '%Y-%m-%d')

def as_character(value):
  # Do whatever as.character does
  return value

pipe("2014-01-01", as_date)
pipe(12, sqrt, lambda x: x**2, as_character)

1
我非常喜欢这个解决方案,因为语法简单易读。这是一个人可以经常输入的东西。我唯一的问题是for循环是否会影响函数组合的性能。 - Alfie González
Python3需要在代码中添加list(list(list(...)))。这几乎是不可能阅读的。反向阅读也是如此。尝试使用fluentpyinfixpy - WestCoastProjects
@StephenBoesch....两个都错了。我不明白为什么你认为需要多次调用list?这里的解决方案根本不返回列表。 同样,pipe函数不会向后读取 - 它是从左到右的。如果你想要从右到左,你需要一个compose函数。同样容易使用相同的原则实现。 - jramm
这绝对不是“两面都错”。Python3默认使用迭代器,因此需要使用list来实现集合的结果。多个mapfilter等需要每个都有一个list,因此会污染代码。pipe是第三方库,因此您的评论不适用于它。 - WestCoastProjects

9

使用 Infix 构建 pipe

正如Sylvain Leroux所示,我们可以使用Infix运算符构建中缀pipe。让我们看看如何实现这一点。

首先,这是来自Tomer Filiba的代码:

Code sample and comments by Tomer Filiba (http://tomerfiliba.com/blog/Infix-Operators/) :

from functools import partial

class Infix(object):
    def __init__(self, func):
        self.func = func
    def __or__(self, other):
        return self.func(other)
    def __ror__(self, other):
        return Infix(partial(self.func, other))
    def __call__(self, v1, v2):
        return self.func(v1, v2)

Using instances of this peculiar class, we can now use a new "syntax" for calling functions as infix operators:

>>> @Infix
... def add(x, y):
...     return x + y
...
>>> 5 |add| 6
管道运算符将前一个对象作为参数传递给后面的对象,因此x %>% f可以转换为f(x)。因此,pipe运算符可以使用Infix定义如下:
In [1]: @Infix
   ...: def pipe(x, f):
   ...:     return f(x)
   ...:
   ...:

In [2]: from math import sqrt

In [3]: 12 |pipe| sqrt |pipe| str
Out[3]: '3.4641016151377544'

关于部分应用的说明

dplyr 中的 %>% 运算符可以将参数传递到函数的第一个参数中,因此

df %>% 
filter(x >= 2) %>%
mutate(y = 2*x)

对应于

df1 <- filter(df, x >= 2)
df2 <- mutate(df1, y = 2*x)

在Python中实现类似的功能最简单的方法是使用柯里化toolz库提供了一个curry装饰器函数,使构建柯里化函数变得容易。

In [2]: from toolz import curry

In [3]: from datetime import datetime

In [4]: @curry
    def asDate(format, date_string):
        return datetime.strptime(date_string, format)
    ...:
    ...:

In [5]: "2014-01-01" |pipe| asDate("%Y-%m-%d")
Out[5]: datetime.datetime(2014, 1, 1, 0, 0)

请注意,|pipe|将参数推入最后一个参数位置,即

x |pipe| f(2)

对应于

f(2, x)

在设计柯里化函数时,静态参数(即可能用于多个示例的参数)应该放在参数列表的前面。

请注意,toolz包括许多预先柯里化的函数,包括来自operator模块的各种函数。

In [11]: from toolz.curried import map

In [12]: from toolz.curried.operator import add

In [13]: range(5) |pipe| map(add(2)) |pipe| list
Out[13]: [2, 3, 4, 5, 6]

在R中大体对应如下:

> library(dplyr)
> add2 <- function(x) {x + 2}
> 0:4 %>% sapply(add2)
[1] 2 3 4 5 6

使用其他中缀分隔符

您可以通过覆盖其他Python操作符方法来更改包围中缀调用的符号。例如,将 __or____ror__ 切换为 __mod____rmod__ 将把 | 运算符更改为 mod 运算符。

In [5]: 12 %pipe% sqrt %pipe% str
Out[5]: '3.4641016151377544'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接