使用Python中缀语法将"管道"输出从一个函数输出到另一个函数

Mal*_*hus 25 python pipeline infix-notation

我正在尝试使用Python/Pandas(作为学习练习)粗略地复制R中的dplyr包.我坚持的是"管道"功能.

在R/dplyr中,这是使用管道运算符完成的%>%,其中x %>% f(y)相当于f(x, y).如果可能,我想使用中缀语法复制它(参见此处).

为了说明,请考虑以下两个功能.

import pandas as pd

def select(df, *args):
    cols = [x for x in args]
    df = df[cols]
    return df

def rename(df, **kwargs):
    for name, value in kwargs.items():
        df = df.rename(columns={'%s' % name: '%s' % value})
    return df
Run Code Online (Sandbox Code Playgroud)

第一个函数采用数据帧并仅返回给定的列.第二个采用数据帧,并重命名给定的列.例如:

d = {'one' : [1., 2., 3., 4., 4.],
     'two' : [4., 3., 2., 1., 3.]}

df = pd.DataFrame(d)

# Keep only the 'one' column.
df = select(df, 'one')

# Rename the 'one' column to 'new_one'.
df = rename(df, one = 'new_one')
Run Code Online (Sandbox Code Playgroud)

要使用管道/中缀语法实现相同的代码,代码将是:

df = df | select('one') \
        | rename(one = 'new_one')
Run Code Online (Sandbox Code Playgroud)

因此,左侧的输出|作为第一个参数传递给右侧的函数.每当我看到这样的事情(例如,这里),它涉及lambda函数.是否可以以相同的方式在函数之间管道Pandas的数据帧?

我知道Pandas有这个.pipe方法,但对我来说重要的是我提供的例子的语法.任何帮助,将不胜感激.

Pau*_*ine 22

使用按位运算or符很难pandas.DataFrame实现它,因为实现它.如果你不介意更换|>>,你可以试试这个:

import pandas as pd

def select(df, *args):
    cols = [x for x in args]
    return df[cols]


def rename(df, **kwargs):
    for name, value in kwargs.items():
        df = df.rename(columns={'%s' % name: '%s' % value})
    return df


class SinkInto(object):
    def __init__(self, function, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
        self.function = function

    def __rrshift__(self, other):
        return self.function(other, *self.args, **self.kwargs)

    def __repr__(self):
        return "<SinkInto {} args={} kwargs={}>".format(
            self.function, 
            self.args, 
            self.kwargs
        )

df = pd.DataFrame({'one' : [1., 2., 3., 4., 4.],
                   'two' : [4., 3., 2., 1., 3.]})
Run Code Online (Sandbox Code Playgroud)

然后你可以这样做:

>>> df
   one  two
0    1    4
1    2    3
2    3    2
3    4    1
4    4    3

>>> df = df >> SinkInto(select, 'one') \
            >> SinkInto(rename, one='new_one')
>>> df
   new_one
0        1
1        2
2        3
3        4
4        4
Run Code Online (Sandbox Code Playgroud)

在Python 3中,您可以滥用unicode:

>>> print('\u01c1')
?
>>> ? = SinkInto
>>> df >> ?(select, 'one') >> ?(rename, one='new_one')
   new_one
0        1
1        2
2        3
3        4
4        4
Run Code Online (Sandbox Code Playgroud)

[更新]

感谢您的答复.是否可以为每个函数创建一个单独的类(如SinkInto)以避免必须将函数作为参数传递?

装饰师怎么样?

def pipe(original):
    class PipeInto(object):
        data = {'function': original}

        def __init__(self, *args, **kwargs):
            self.data['args'] = args
            self.data['kwargs'] = kwargs

        def __rrshift__(self, other):
            return self.data['function'](
                other, 
                *self.data['args'], 
                **self.data['kwargs']
            )

    return PipeInto


@pipe
def select(df, *args):
    cols = [x for x in args]
    return df[cols]


@pipe
def rename(df, **kwargs):
    for name, value in kwargs.items():
        df = df.rename(columns={'%s' % name: '%s' % value})
    return df
Run Code Online (Sandbox Code Playgroud)

现在你可以装饰任何带有DataFrame第一个参数的函数:

>>> df >> select('one') >> rename(one='first')
   first
0      1
1      2
2      3
3      4
4      4
Run Code Online (Sandbox Code Playgroud)

Python太棒了!

我知道像Ruby这样的语言"非常富有表现力",它鼓励人们把每个程序都写成新的DSL,但这在Python中是不受欢迎的.许多Python教徒认为运算符重载是出于不同的目的而作为一种罪恶的亵渎.

[更新]

用户OHLÁLÁ没有留下深刻的印象:

此解决方案的问题是当您尝试调用函数而不是管道时.- 哦啦啦

您可以实现dunder-call方法:

def __call__(self, df):
    return df >> self
Run Code Online (Sandbox Code Playgroud)

然后:

>>> select('one')(df)
   one
0  1.0
1  2.0
2  3.0
3  4.0
4  4.0
Run Code Online (Sandbox Code Playgroud)

看起来好像不容易取悦OHLÁLÁ:

在这种情况下,您需要明确调用该对象:
select('one')(df)有没有办法避免这种情况?- 哦啦啦

好吧,我可以想到一个解决方案,但有一个警告:你的原始函数不能采取第二个位置参数,这是一个pandas数据帧(关键字参数是好的).让__new__我们PipeInto在docorator中为我们的类添加一个方法,测试第一个参数是否是一个数据帧,如果是,那么我们只需用参数调用原始函数:

def __new__(cls, *args, **kwargs):
    if args and isinstance(args[0], pd.DataFrame):
        return cls.data['function'](*args, **kwargs)
    return super().__new__(cls)
Run Code Online (Sandbox Code Playgroud)

它似乎工作,但可能有一些我无法发现的缺点.

>>> select(df, 'one')
   one
0  1.0
1  2.0
2  3.0
3  4.0
4  4.0

>>> df >> select('one')
   one
0  1.0
1  2.0
2  3.0
3  4.0
4  4.0
Run Code Online (Sandbox Code Playgroud)


lga*_*ier 10

虽然我不禁提到在Python中使用dplyr可能是最接近Python中的dplyr(它有rshift运算符,但作为一个噱头),我还想指出管道运算符可能只是R中必需的,因为它使用泛型函数而不是方法作为对象属性.方法链接基本上相同,而不必覆盖运算符:

dataf = (DataFrame(mtcars).
         filter('gear>=3').
         mutate(powertoweight='hp*36/wt').
         group_by('gear').
         summarize(mean_ptw='mean(powertoweight)'))
Run Code Online (Sandbox Code Playgroud)

请注意,在一对括号之间包裹链可以将其分成多行,而不需要\在每一行上进行尾随处理.

  • 这很不错,但不幸的是,我必须使用基本的Python函数/对象,它们不能像这样工作.这就是我正在寻找合适的管道系统的原因. (3认同)

mhs*_*vat 6

您可以使用sspipe库,并使用以下语法:

from sspipe import p
df = df | p(select, 'one') \
        | p(rename, one = 'new_one')
Run Code Online (Sandbox Code Playgroud)


jra*_*amm 6

我强烈反对这样做或这里建议的任何答案,而只是pipe在标准 python 代码中实现一个函数,而不需要操作符欺骗、装饰器或其他什么:

def pipe(first, *args):
  for fn in args:
    first = fn(first)
  return first
Run Code Online (Sandbox Code Playgroud)

有关更多背景信息,请参阅我的答案: /sf/answers/4243508811/

重载运算符,涉及外部库以及其他不利于代码可读性、可维护性、可测试性和Python风格的内容。如果我想在 python 中做某​​种管道,我不想做更多的事情pipe(input, fn1, fn2, fn3)。这是我能想到的最具可读性和最强大的解决方案。如果我们公司中的某人只是为了做一个管道而在生产中提交了操作符重载或新的依赖项,那么它将立即恢复,并且他们将被判处在本周剩余时间进行 QA 检查:D 如果您真的真的必须使用某种管道运算符,那么也许你有更大的问题,Python 不是适合你的用例的语言......