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

时间:2015-11-11 19:29:16

标签: python pipeline infix-notation

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

在R / dplyr中,这是使用管道操作符%>%完成的,其中x %>% f(y)等同于f(x, y)。如果可能,我想使用中缀语法复制它(请参阅here)。

为了说明,请考虑以下两个函数。

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

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

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')

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

df = df | select('one') \
        | rename(one = 'new_one')

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

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

7 个答案:

答案 0 :(得分:18)

使用按位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.]})

然后你可以这样做:

>>> 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

在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

[更新]

  

感谢您的回复。是否可以为每个函数创建一个单独的类(如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

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

>>> df >> select('one') >> rename(one='first')
   first
0      1
1      2
2      3
3      4
4      4

Python太棒了!

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

[更新]

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

  

此解决方案的问题在于您尝试调用函数而不是管道。 - OHLÁLÁ

您可以实施dunder-call方法:

def __call__(self, df):
    return df >> self

然后:

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

看起来很难取悦OHLÁLÁ:

  

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

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

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

它似乎有用,但可能有一些我无法发现的缺点。

>>> 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

答案 1 :(得分:9)

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

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

注意在一对括号之间包裹链可以将它分成多行,而不需要在每一行上都有尾随\

答案 2 :(得分:1)

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

from sspipe import p
df = df | p(select, 'one') \
        | p(rename, one = 'new_one')

答案 3 :(得分:1)

我一直在从 Python 中的 R 移植数据包(dplyr、tidyr、tibble 等):

https://github.com/pwwang/datar

如果您熟悉 R 中的那些包,并想将其应用到 Python 中,那么它就在这里为您提供:

void findAns(int n, int peg1, int peg2, int peg3)
{
    if(n<1)
        return;
        
    findAns(n-1,peg1,peg2,peg3);
    printf("Move(%d->%d)n",peg1,peg3);
    findAns(n-1,peg2,peg1,peg3);
    printf("Move(%d->%d)n",peg3,peg2);
    
    findAns(n-1,peg1,peg2,peg3);
    
}

输出:

from datar.all import *

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

df = df >> select(f.one) >> rename(new_one=f.one)
print(df)

答案 4 :(得分:0)

我无法找到这样做的内置方式,所以我创建了一个使用__call__运算符的类,因为它支持*args/**kwargs

class Pipe:
    def __init__(self, value):
        """
        Creates a new pipe with a given value.
        """
        self.value = value
    def __call__(self, func, *args, **kwargs):
        """
        Creates a new pipe with the value returned from `func` called with
        `args` and `kwargs` and it's easy to save your intermedi.
        """
        value = func(self.value, *args, **kwargs)
        return Pipe(value)

语法需要一些习惯,但它允许管道。

def get(dictionary, key):
    assert isinstance(dictionary, dict)
    assert isinstance(key, str)
    return dictionary.get(key)

def keys(dictionary):
    assert isinstance(dictionary, dict)
    return dictionary.keys()

def filter_by(iterable, check):
    assert hasattr(iterable, '__iter__')
    assert callable(check)
    return [item for item in iterable if check(item)]

def update(dictionary, **kwargs):
    assert isinstance(dictionary, dict)
    dictionary.update(kwargs)
    return dictionary


x = Pipe({'a': 3, 'b': 4})(update, a=5, c=7, d=8, e=1)
y = (x
    (keys)
    (filter_by, lambda key: key in ('a', 'c', 'e', 'g'))
    (set)
    ).value
z = x(lambda dictionary: dictionary['a']).value

assert x.value == {'a': 5, 'b': 4, 'c': 7, 'd': 8, 'e': 1}
assert y == {'a', 'c', 'e'}
assert z == 5

答案 5 :(得分:0)

我会坚决反对这样做或此处提出的任何答案,而只是在标准python代码中实现pipe函数,而不会引起操作员的欺骗,装饰或其他错误:

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

在此处查看我的答案以获取更多背景信息: https://stackoverflow.com/a/60621554/2768350

重载运算符,涉及外部库,而那些不能使代码的可读性差,可维护性差,可测试性差和Python语言少的东西。 如果我想在python中进行某种形式的管道操作,我只想做pipe(input, fn1, fn2, fn3)。那是我能想到的最可读和最强大的解决方案。 如果我们公司中的某人将操作员重载或对生产 just 的新依赖项进行了处理,则该管道将立即恢复,并且他们将被判处在本周剩余时间内进行质量检查:D 如果您真的真的必须对管道使用某种运算符,那么也许您遇到了更大的问题,并且Python不是您的用例的正确语言...

答案 6 :(得分:0)

一个老问题,但我仍然很感兴趣(来自 R)。因此,尽管受到纯粹主义者的反对,这里还是一个受 http://tomerfiliba.com/blog/Infix-Operators/

启发的矮个子
class FuncPipe:
    class Arg:
        def __init__(self, arg):
            self.arg = arg
        def __or__(self, func):
            return func(self.arg)

    def __ror__(self, arg):
        return self.Arg(arg)
pipe = FuncPipe()

然后

1 |pipe| \
  (lambda x: return x+1) |pipe| \
  (lambda x: return 2*x)

返回

4