簡體   English   中英

使用 Python 中綴語法從一個函數到另一個函數的“管道”輸出

[英]"Piping" output from one function to another using Python infix syntax

我正在嘗試使用 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

第一個函數接受一個數據框並只返回給定的列。 第二個需要一個數據框,並重命名給定的列。 例如:

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

所以來自|左側的輸出作為第一個參數傳遞給右側的函數。 每當我看到這樣的事情完成時(例如這里),它涉及到 lambda 函數。 是否可以以相同的方式在函數之間傳輸 Pandas 的數據幀?

我知道 Pandas 有.pipe方法,但對我來說重要的是我提供的示例的語法。 任何幫助,將不勝感激。

使用按位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

蟒蛇真棒!

我知道像 Ruby 這樣的語言“非常有表現力”,它鼓勵人們將每個程序編寫為新的 DSL,但這在 Python 中有點不受歡迎。 許多 Python 專家將出於不同目的的運算符重載視為有罪的褻瀆。

[更新]

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

看來要取悅奧拉拉並不容易:

在這種情況下,您需要顯式調用對象:
select('one')(df)有沒有辦法避免這種情況? - 哦啦啦

好吧,我可以想到一個解決方案,但有一個警告:您的原始函數不得采用第二個位置參數,即 Pandas 數據框(關鍵字參數可以)。 讓我們在__new__中的PipeInto類中添加一個__new__方法,用於測試第一個參數是否是數據幀,如果是,則我們只需使用參數調用原始函數:

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

雖然我不禁提到在 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)'))

請注意,將鏈包在一對括號之間可以讓您將其分成多行,而無需每行尾隨\\

您可以使用sspipe庫,並使用以下語法:

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

我強烈反對這樣做或這里建議的任何答案,只在標准 python 代碼中實現pipe函數,沒有操作符技巧、裝飾器或其他什么:

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

有關更多背景信息,請在此處查看我的答案: https : //stackoverflow.com/a/60621554/2768350

重載運算符,涉及外部庫以及不會使代碼可讀性差、可維護性差、可測試性差和 Pythonic 少的東西。 如果我想在 python 中做某​​種管道,我不想做的不僅僅是pipe(input, fn1, fn2, fn3) 這是我能想到的最可讀和最強大的解決方案。 如果有人在我們公司致力於運算符重載或新的依賴於生產只是做一個管道,它會立即得到恢復,他們將被判處做QA檢查一周的休息:d如果你真的真的真的必須使用某種管道運算符,那么您可能會遇到更大的問題,而 Python 不是您用例的正確語言...

我一直在從 Python 中的 R 移植數據包(dplyr、tidyr、tibble 等):

https://github.com/pwwang/datar

如果您熟悉 R 中的那些包,並想在 python 中應用它,那么它為您提供:

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)

輸出:

   new_one
0      1.0
1      2.0
2      3.0
3      4.0
4      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

一個老問題,但我仍然感興趣(來自 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 

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM