"Трубопроводный" вывод из одной функции в другую с использованием инфиксного синтаксиса Python

Я пытаюсь примерно скопировать пакет dplyr из R, используя Python/Pandas (в качестве учебного упражнения). Что-то, на чем я застрял - это функциональность "обвязки".

В R/dplyr это делается с помощью pipe-оператора %>%, где 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')

Чтобы добиться того же, используя синтаксис pipe/infix, код должен быть:

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

Таким образом, выход с левой стороны | передается в качестве первого аргумента функции справа. Всякий раз, когда я вижу что-то подобное ( здесь, например), это включает лямбда-функции. Можно ли таким же образом передавать данные между кадрами Pandas?

Я знаю, что у Панд есть .pipe метод, но для меня важен синтаксис приведенного мною примера. Любая помощь будет оценена.

7 ответов

Решение

Это трудно реализовать с помощью побитового 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 вы можете злоупотреблять юникодом:

>>> 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 это не одобряется. Многие питонисты считают перегрузку операторов для других целей греховным богохульством.

[Обновить]

Пользователь 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 класс внутри документа, который проверяет, является ли первый аргумент фреймом данных, и если это так, то мы просто вызываем исходную функцию с аргументами:

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

Хотя я не могу не упомянуть, что использование dplyr в Python может быть ближе всего к dplyr в Python (он имеет оператор rshift, но в качестве трюка), я хотел бы также отметить, что оператор pipe может быть только необходимо в 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')

Я категорически возражаю против этого или любого из предложенных здесь ответов и просто реализуйте pipe функция в стандартном коде Python, без обмана операторов, декораторов и прочего:

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

См. Мой ответ здесь для получения дополнительной информации: /questions/40868996/funktsionalnyie-kanalyi-v-python-takie-kak-ot-rs-dplyr/55388208#55388208

Операторы перегрузки, включающие внешние библиотеки и то, что не служит, чтобы сделать код менее читаемым, менее удобным в обслуживании, менее тестируемым и менее питоническим. Если я хочу создать какой-то канал на Python, я бы не хотел делать больше, чем pipe(input, fn1, fn2, fn3). Это самое читаемое и надежное решение, о котором я могу думать. Если кто-то в нашей компании ввел перегрузку оператора или новые зависимости в продакшн только для того, чтобы выполнить конвейер, он немедленно откатился бы, и его приговорили бы к проверкам QA до конца недели:D Если вам действительно действительно нужно использовать какой-то вид оператор для канала, тогда, возможно, у вас есть более серьезные проблемы, и Python не подходит для вашего варианта использования...

Я переносил пакеты данных (dplyr, tidyr, tibble и т. д.) из R в python:

https://github.com/pwwang/датар

Если вы знакомы с этими пакетами в 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 
Другие вопросы по тегам