python中的功能管道,例如R的magritrr中的%>%


83

在R中(感谢magritrr),您现在可以通过通过更实用的管道语法执行操作%>%。这意味着无需编码:

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

您也可以这样做:

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

对我来说,这更具可读性,并且扩展到数据框之外的用例。python语言是否支持类似功能?


1
好问题。我对功能具有更多参数的情况特别感兴趣。作为crime_by_state %>% filter(State=="New York", Year==2005) ...从年底dplyr如何取代我最常用的成语- [R
Piotr Migdal

1
当然,可以使用很多lambda,映射和简化来做到这一点(这样做很简单),但是简洁和可读性是要点。
Piotr Migdal,2015年

12
有问题的包装是magrittr。
piccolbo 2015年

1
是的,出于同样的原因,曾经编写的每个R包都由Hadley编写。他比较有名。(此处伪装成
令人

1
请参阅正在解决此问题的stackoverflow.com/questions/33658355/…的答案。
Piotr Migdal

Answers:


34

一种可行的方法是使用名为的模块macropy。Macropy允许您将转换应用于所编写的代码。因此a | b可以转换为b(a)。这具有许多优点和缺点。

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

主要缺点是,宏需要某种特定的激活方式才能起作用(稍后会提到)。与更快的运行时间相比,源代码的解析在计算上更加复杂,因此程序将需要更长的启动时间。最后,它添加了一种语法样式,这意味着不熟悉宏的程序员可能会发现您的代码更难理解。

示例代码:

运行

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)

2
听起来不错,但正如我所见,它仅适用于Python 2.7(不适用于Python 3.4)。
Piotr Migdal

3
我创建了一个没有依赖项的较小库,该库的作用与@fpipe装饰器相同,但重新定义了右移(>>)而不是or(|):pypi.org/project/pipeop
Robin Hilliard,

因使用多个装饰器而需要第三方库而被否决,这是一个非常简单的问题的非常复杂的解决方案。加上它是仅python 2的解决方案。可以肯定的是,香草python解决方案也会更快。
jramm

37

管道是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到位。


4
不幸的是,这仅适用于数据帧,因此我无法将其指定为正确答案。但是在这里要提一下,我想到的主要用例是将其应用于数据框。
cantdutchthis

22

PyToolz [doc]允许任意组合的管道,只是它们不是使用该管道操作符语法定义的。

按照上面的链接进行快速入门。这是一个视频教程:http : //pyvideo.org/video/2858/functional-programming-in-python-with-pytoolz

In [1]: from toolz import pipe

In [2]: from math import sqrt

In [3]: pipe(12, sqrt, str)
Out[3]: '3.4641016151377544'

1
PyToolz是一个很好的指针。话虽这么说,一个链接已死,另一个链接即将死
akhmed

2
他的基本URL似乎是:http : //matthewrocklin.com/blog和PyToolz toolz.readthedocs.io/en/latest。啊,互联网的短暂性……
smci

18

python语言是否支持类似功能?

“更多功能的管道语法”真的是更“功能”的语法吗?我会说它向R添加了“中缀”语法。

话虽这么说,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

如果只想将此用于个人脚本,则可能要考虑使用Coconut而不是Python。

Coconut是Python的超集。因此,您可以使用Coconut的pipe运算符|>,而完全忽略其余的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 '18

1
@ jimbo1qaz如果仍然存在此问题,请尝试print(1 |> isinstance$(int))或建议使用1 |> isinstance$(int) |> print
所罗门·乌科

@Solomon Ucko您的回答是错误的。自$以来的1 |> print$(2)调用print(2, 1)映射到Python局部函数。但是我想要print(1, 2)匹配UFCS和magrittr。动机:1 |> add(2) |> divide(6)应为0.5,我不需要括号。
nyanpasu64

@ jimbo1qaz是的,看来我以前的评论是错误的。您实际上需要1 |> isinstance$(?, int) |> print。对于其他示例:1 |> print$(?, 2)1 |> (+)$(?, 2) |> (/)$(?, 6)。我认为您不能为部分应用而避免使用括号。
所罗门·乌科

看着多么丑陋都|>(+)$(?, 2)是的,我得出的结论是,编程语言和数学建立不希望我用这种类型的语法,并使其甚至丑陋不是诉诸一对括号。如果语法更好(例如Dlang具有UFCS但IDK有关算术函数,或者Python具有..管道运算符),我会使用它。
nyanpasu64

11

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包,以便他们将在语法非常相似。
BigDataScientist

9

我错过了|>Elixir的管道运算符,因此创建了一个简单的函数修饰器(约50行代码),该>>编译器在编译时使用ast库和compile / exec将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


9

您可以使用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)

8

建筑pipeInfix

正如Sylvain Leroux所暗示的那样,我们可以使用Infix运算符来构造一个中缀pipe。让我们看看这是如何完成的。

首先,这是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

管道运算符将前面的对象作为参数传递给跟随管道的对象,因此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'

关于部分申请的注意事项

%>%由运营商dpylr推动的参数通过一个函数的第一个参数,所以

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

对应于

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

在Python中实现类似功能的最简单方法是使用currying。该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运算符方法来更改围绕Infix调用的符号。例如,将__or__和切换__ror____mod____rmod__|操作员更改为操作mod员。

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

6

添加我的2c。我个人使用软件包fn进行功能样式编程。您的示例翻译成

from fn import F, _
from math import sqrt

(F(sqrt) >> _**2 >> str)(12)

F是带有功能样式语法糖的包装器类,用于部分应用和组成。_是用于匿名函数的Scala样式构造函数(类似于Python的lambda);它表示一个变量,因此您可以_在一个表达式中组合多个对象以获得具有更多参数的函数(例如_ + _等效于lambda a, b: a + b)。F(sqrt) >> _**2 >> str导致Callable对象可以根据需要多次使用。


我正在寻找的东西-甚至提到了scala作为示例。立即尝试
StephenBoesch

@javadba很高兴您发现此功能有用。请注意,这_不是100%灵活的:它不支持所有Python运算符。另外,如果您打算在_交互式会话中使用它,则应使用另一个名称(例如from fn import _ as var)将其导入,因为大多数(如果不是全部)交互式Python shell都_用来表示最后一个未分配的返回值,从而使导入的对象不透明。
Eli Korvigo

5

无需第三方库或令人费解的操作员技巧即可实现管道功能-您可以自己轻松地掌握基础知识。

让我们从定义实际的管道功能开始。从本质上讲,这只是一种以逻辑顺序而不是标准的“由内而外”顺序来表达一系列函数调用的方法。

例如,让我们看一下这些功能:

def one(value):
  return value

def two(value):
  return 2*value

def three(value):
  return 3*value

不是很有趣,但是假设发生了有趣的事情value。我们想按顺序调用它们,将每个输出传递给下一个。在香草python中,将是:

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

它不是难以置信的可读性,对于更复杂的管道,它会变得更糟。因此,这是一个带有初始参数的简单管道函数,以及将其应用到的一系列函数:

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)

3

一种替代解决方案是使用工作流工具dask。虽然在语法上不如...

var
| do this
| then do that

...它仍然允许您的变量沿链向下流动,使用dask在可能的情况下提供了并行化的额外好处。

这是我使用dask来完成管道链模式的方法:

import dask

def a(foo):
    return foo + 1
def b(foo):
    return foo / 2
def c(foo,bar):
    return foo + bar

# pattern = 'name_of_behavior': (method_to_call, variables_to_pass_in, variables_can_be_task_names)
workflow = {'a_task':(a,1),
            'b_task':(b,'a_task',),
            'c_task':(c,99,'b_task'),}

#dask.visualize(workflow) #visualization available. 

dask.get(workflow,'c_task')

# returns 100

在使用elixir之后,我想在Python中使用管道模式。这不是完全相同的模式,但是类似,就像我说的那样,还带来了并行化的更多好处。如果您告诉dask在工作流程中获得一个不依赖其他人先运行的任务,则它们将并行运行。

如果您想要更简单的语法,则可以将其包装在可以为您命名任务的内容中。当然,在这种情况下,您将需要所有函数将管道作为第一个参数,并且将失去任何并行化的好处。但是,如果您认为可以,可以执行以下操作:

def dask_pipe(initial_var, functions_args):
    '''
    call the dask_pipe with an init_var, and a list of functions
    workflow, last_task = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})
    workflow, last_task = dask_pipe(initial_var, [function_1, function_2])
    dask.get(workflow, last_task)
    '''
    workflow = {}
    if isinstance(functions_args, list):
        for ix, function in enumerate(functions_args):
            if ix == 0:
                workflow['task_' + str(ix)] = (function, initial_var)
            else:
                workflow['task_' + str(ix)] = (function, 'task_' + str(ix - 1))
        return workflow, 'task_' + str(ix)
    elif isinstance(functions_args, dict):
        for ix, (function, args) in enumerate(functions_args.items()):
            if ix == 0:
                workflow['task_' + str(ix)] = (function, initial_var)
            else:
                workflow['task_' + str(ix)] = (function, 'task_' + str(ix - 1), *args )
        return workflow, 'task_' + str(ix)

# piped functions
def foo(df):
    return df[['a','b']]
def bar(df, s1, s2):
    return df.columns.tolist() + [s1, s2]
def baz(df):
    return df.columns.tolist()

# setup 
import dask
import pandas as pd
df = pd.DataFrame({'a':[1,2,3],'b':[1,2,3],'c':[1,2,3]})

现在,使用此包装器,您可以按照以下两种语法模式制作管道:

# wf, lt = dask_pipe(initial_var, [function_1, function_2])
# wf, lt = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})

像这样:

# test 1 - lists for functions only:
workflow, last_task =  dask_pipe(df, [foo, baz])
print(dask.get(workflow, last_task)) # returns ['a','b']

# test 2 - dictionary for args:
workflow, last_task = dask_pipe(df, {foo:[], bar:['string1', 'string2']})
print(dask.get(workflow, last_task)) # returns ['a','b','string1','string2']

一个问题是您不能将函数作为参数传递:(
Legit Stack '18

2

有非常好的pipe位置模块https://pypi.org/project/pipe/ 它重载| 操作员,并提供许多类似的管道功能add, first, where, tail

>>> [1, 2, 3, 4] | where(lambda x: x % 2 == 0) | add
6

>>> sum([1, [2, 3], 4] | traverse)
10

另外,编写自己的管道函数非常容易

@Pipe
def p_sqrt(x):
    return sqrt(x)

@Pipe
def p_pr(x):
    print(x)

9 | p_sqrt | p_pr
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.