"Трубопроводный" вывод из одной функции в другую с использованием инфиксного синтаксиса 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