Функциональные трубы в питоне, такие как%>% от R magritrr

В R (благодаря magritrr) теперь вы можете выполнять операции с более функциональным синтаксисом трубопровода через %>%. Это означает, что вместо кодирования это:

> as.Date("2014-01-01")
> as.character((sqrt(12)^2)

Вы также можете сделать это:

> "2014-01-01" %>% as.Date 
> 12 %>% sqrt %>% .^2 %>% as.character

Для меня это более читабельно, и это распространяется на случаи использования за рамками данных. Поддерживает ли язык Python нечто подобное?

Ответ 1

Один из возможных способов сделать это - использовать модуль macropy. Macropy позволяет применять преобразования к написанному вами коду. Таким образом, a | b можно преобразовать в b(a). Это имеет ряд преимуществ и недостатков.

По сравнению с решением, упомянутым Sylvain Leroux, основное преимущество заключается в том, что вам не нужно создавать объекты инфикс для функций, которые вас интересуют, - просто отметьте области кода, которые вы намерены использовать для преобразования. Во-вторых, поскольку преобразование применяется во время компиляции, а не во время выполнения, преобразованный код не страдает накладными расходами во время выполнения - вся работа выполняется, когда байт-код сначала создается из исходного кода.

Основными недостатками являются то, что для макропины требуется определенный способ активации (см. ниже). В отличие от более быстрой работы, синтаксический анализ исходного кода является более сложным с вычислительной точки зрения, поэтому для запуска программы потребуется больше времени. Наконец, он добавляет синтаксический стиль, который означает, что программисты, которые не знакомы с макропией, могут найти ваш код более сложным для понимания.

Пример кода:

run.py

import macropy.activate 
# Activates macropy, modules using macropy cannot be imported before this statement
# in the program.
import target
# import the module using macropy

target.py

from fpipe import macros, fpipe
from macropy.quick_lambda import macros, f
# The `from module import macros, ...` must be used for macropy to know which 
# macros it should apply to your code.
# Here two macros have been imported `fpipe`, which does what you want
# and `f` which provides a quicker way to write lambdas.

from math import sqrt

# Using the fpipe macro in a single expression.
# The code between the square braces is interpreted as - str(sqrt(12))
print fpipe[12 | sqrt | str] # prints 3.46410161514

# using a decorator
# All code within the function is examined for `x | y` constructs.
x = 1 # global variable
@fpipe
def sum_range_then_square():
    "expected value (1 + 2 + 3)**2 -> 36"
    y = 4 # local variable
    return range(x, y) | sum | f[_**2]
    # `f[_**2]` is macropy syntax for -- `lambda x: x**2`, which would also work here

print sum_range_then_square() # prints 36

# using a with block.
# same as a decorator, but for limited blocks.
with fpipe:
    print range(4) | sum # prints 6
    print 'a b c' | f[_.split()] # prints ['a', 'b', 'c']

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

fpipe.py

from macropy.core.macros import *
from macropy.core.quotes import macros, q, ast

macros = Macros()

@macros.decorator
@macros.block
@macros.expr
def fpipe(tree, **kw):

    @Walker
    def pipe_search(tree, stop, **kw):
        """Search code for bitwise or operators and transform `a | b` to `b(a)`."""
        if isinstance(tree, BinOp) and isinstance(tree.op, BitOr):
            operand = tree.left
            function = tree.right
            newtree = q[ast[function](ast[operand])]
            return newtree

    return pipe_search.recurse(tree)

Ответ 2

Трубы - это новая функция в Pandas 0.16.2.

Пример:

import pandas as pd
from sklearn.datasets import load_iris

x = load_iris()
x = pd.DataFrame(x.data, columns=x.feature_names)

def remove_units(df):
    df.columns = pd.Index(map(lambda x: x.replace(" (cm)", ""), df.columns))
    return df

def length_times_width(df):
    df['sepal length*width'] = df['sepal length'] * df['sepal width']
    df['petal length*width'] = df['petal length'] * df['petal width']

x.pipe(remove_units).pipe(length_times_width)
x

NB: версия Pandas сохраняет семантику ссылок Python. Поэтому length_times_width не нуждается в возвращаемом значении; он изменяет x на месте.

Ответ 3

Поддерживает ли язык python что-то подобное?

"более функциональный синтаксис трубопроводов" - это действительно более "функциональный" синтаксис? Я бы сказал, что вместо этого добавляет синтаксис "infix" к R.

Говоря, грамматика Python не имеет прямой поддержки нотации infix за пределами стандартных операторов.


Если вам действительно нужно что-то подобное, вы должны взять этот код от Tomer Filiba в качестве отправной точки для реализации своей собственной нотации infix:

Пример кода и комментарии Томера Филибы (http://tomerfiliba.com/blog/Infix-Operators/):

from functools import partial

class Infix(object):
    def __init__(self, func):
        self.func = func
    def __or__(self, other):
        return self.func(other)
    def __ror__(self, other):
        return Infix(partial(self.func, other))
    def __call__(self, v1, v2):
        return self.func(v1, v2)

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

>>> @Infix
... def add(x, y):
...     return x + y
...
>>> 5 |add| 6

Ответ 4

PyToolz [doc] позволяет произвольно компоновать каналы, только они не определены с этим синтаксисом оператора канала.

Перейдите по ссылке выше для быстрого запуска. А вот видеоурок: http://pyvideo.org/video/2858/functional-programming-in-python-with-pytoolz

In [1]: from toolz import pipe

In [2]: from math import sqrt

In [3]: pipe(12, sqrt, str)
Out[3]: '3.4641016151377544'

Ответ 5

Если вы просто хотите это для личных сценариев, вы можете рассмотреть возможность использования Coconut вместо Python.

Кокос является надмножеством Python. Поэтому вы можете использовать оператор Coconut pipe |>, полностью игнорируя остальную часть языка Coconut.

Например:

def addone(x):
    x + 1

3 |> addone

компилируется в

# lots of auto-generated header junk

# Compiled Coconut: -----------------------------------------------------------

def addone(x):
    return x + 1

(addone)(3)

Ответ 6

Строительная pipe с Infix

Как намекнул Сильвен Леру, мы можем использовать оператор Infix для построения pipe инфикса. Давайте посмотрим, как это достигается.

Во-первых, вот код от Томера Филиба

Пример кода и комментарии Томера Филиба (http://tomerfiliba.com/blog/Infix-Operators/):

from functools import partial

class Infix(object):
    def __init__(self, func):
        self.func = func
    def __or__(self, other):
        return self.func(other)
    def __ror__(self, other):
        return Infix(partial(self.func, other))
    def __call__(self, v1, v2):
        return self.func(v1, v2)

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

>>> @Infix
... def add(x, y):
...     return x + y
...
>>> 5 |add| 6

Оператор канала передает предыдущий объект в качестве аргумента объекту, который следует за каналом, поэтому x %>% f можно преобразовать в f(x). Следовательно, оператор pipe может быть определен с помощью Infix следующим образом:

In [1]: @Infix
   ...: def pipe(x, f):
   ...:     return f(x)
   ...:
   ...:

In [2]: from math import sqrt

In [3]: 12 |pipe| sqrt |pipe| str
Out[3]: '3.4641016151377544'

Примечание о частичном применении

Оператор %>% из dpylr проталкивает аргументы через первый аргумент в функции, поэтому

df %>% 
filter(x >= 2) %>%
mutate(y = 2*x)

соответствует

df1 <- filter(df, x >= 2)
df2 <- mutate(df1, y = 2*x)

Самый простой способ достичь чего-то похожего в Python - это использовать карри. Библиотека toolz предоставляет функцию декоратора curry которая упрощает создание карри-функций.

In [2]: from toolz import curry

In [3]: from datetime import datetime

In [4]: @curry
    def asDate(format, date_string):
        return datetime.strptime(date_string, format)
    ...:
    ...:

In [5]: "2014-01-01" |pipe| asDate("%Y-%m-%d")
Out[5]: datetime.datetime(2014, 1, 1, 0, 0)

Обратите внимание, что |pipe| толкает аргументы в последнюю позицию аргумента, то есть

x |pipe| f(2)

соответствует

f(2, x)

При разработке каррированных функций статические аргументы (то есть аргументы, которые могут использоваться для многих примеров) должны быть помещены ранее в список параметров.

Обратите внимание, что toolz включает в себя множество предварительно каррированных функций, в том числе различные функции из модуля operator.

In [11]: from toolz.curried import map

In [12]: from toolz.curried.operator import add

In [13]: range(5) |pipe| map(add(2)) |pipe| list
Out[13]: [2, 3, 4, 5, 6]

что примерно соответствует следующему в R

> library(dplyr)
> add2 <- function(x) {x + 2}
> 0:4 %>% sapply(add2)
[1] 2 3 4 5 6

Использование других разделителей инфиксов

Вы можете изменить символы, которые окружают вызов Infix, переопределив другие методы оператора Python. Например, переключение __or__ и __ror__ на __mod__ и __rmod__ изменит | оператор mod оператора.

In [5]: 12 %pipe% sqrt %pipe% str
Out[5]: '3.4641016151377544'

Ответ 7

Я пропустил оператор канала |> Elixir, поэтому я создал простой декоратор функций (~ 50 строк кода), который по-новому интерпретирует >> оператор правого сдвига Python как очень похожий на Elixir канал во время компиляции с использованием библиотеки ast и compile/exec:

from pipeop import pipes

def add3(a, b, c):
    return a + b + c

def times(a, b):
    return a * b

@pipes
def calc()
    print 1 >> add3(2, 3) >> times(4)  # prints 24

Все, что он делает, это переписывает a >> b(...) как b(a,...).

https://pypi.org/project/pipeop/

https://github.com/robinhilliard/pipes

Ответ 8

Вы можете использовать библиотеку sspipe. Он выставляет два объекта p и px. Подобно x %>% f(y,z), вы можете написать x | p(f, y, z) x | p(f, y, z) и аналогично x %>%.^2 вы можете написать x | px**2 x | px**2.

from sspipe import p, px
from math import sqrt

12 | p(sqrt) | px ** 2 | p(str)

Ответ 9

Добавление моего 2с. Я лично использую пакет fn для программирования функционального стиля. Ваш пример переводится на

from fn import F, _
from math import sqrt

(F(sqrt) >> _**2 >> str)(12)

F - класс-оболочка с синтаксическим сахаром функционального стиля для частичного применения и композиции. _ - конструктор в стиле Scala для анонимных функций (аналог lambda Python); он представляет переменную, следовательно, вы можете объединить несколько объектов _ в одном выражении, чтобы получить функцию с большим количеством аргументов (например, _ + _ эквивалентно lambda a, b: a + b выражению lambda a, b: a + b). F(sqrt) >> _**2 >> str приводит к объекту Callable который можно использовать столько раз, сколько вы хотите.

Ответ 10

Одним из альтернативных решений было бы использование инструмента рабочего процесса dask. Хотя это не так синтаксически весело, как...

var
| do this
| then do that

... он все еще позволяет вашей переменной течь вниз по цепочке, а использование dask дает дополнительное преимущество распараллеливания, где это возможно.

Вот как я использую dask для создания паттерна цепочки труб:

import dask

def a(foo):
    return foo + 1
def b(foo):
    return foo / 2
def c(foo,bar):
    return foo + bar

# pattern = 'name_of_behavior': (method_to_call, variables_to_pass_in, variables_can_be_task_names)
workflow = {'a_task':(a,1),
            'b_task':(b,'a_task',),
            'c_task':(c,99,'b_task'),}

#dask.visualize(workflow) #visualization available. 

dask.get(workflow,'c_task')

# returns 100

Поработав с эликсиром, я захотел использовать паттерн трубопровода в Python. Это не совсем та же схема, но она похожа и, как я уже сказал, дает дополнительные преимущества распараллеливания; если вы дадите dask команду на выполнение задачи в вашем рабочем процессе, которая не зависит от запуска других, они будут выполняться параллельно.

Если вам нужен более простой синтаксис, вы можете заключить его во что-нибудь, что позаботится о наименовании задач для вас. Конечно, в этой ситуации вам понадобятся все функции, чтобы принять канал в качестве первого аргумента, и вы потеряете какую-либо выгоду от параллизации. Но если вы согласны с этим, вы можете сделать что-то вроде этого:

def dask_pipe(initial_var, functions_args):
    '''
    call the dask_pipe with an init_var, and a list of functions
    workflow, last_task = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})
    workflow, last_task = dask_pipe(initial_var, [function_1, function_2])
    dask.get(workflow, last_task)
    '''
    workflow = {}
    if isinstance(functions_args, list):
        for ix, function in enumerate(functions_args):
            if ix == 0:
                workflow['task_' + str(ix)] = (function, initial_var)
            else:
                workflow['task_' + str(ix)] = (function, 'task_' + str(ix - 1))
        return workflow, 'task_' + str(ix)
    elif isinstance(functions_args, dict):
        for ix, (function, args) in enumerate(functions_args.items()):
            if ix == 0:
                workflow['task_' + str(ix)] = (function, initial_var)
            else:
                workflow['task_' + str(ix)] = (function, 'task_' + str(ix - 1), *args )
        return workflow, 'task_' + str(ix)

# piped functions
def foo(df):
    return df[['a','b']]
def bar(df, s1, s2):
    return df.columns.tolist() + [s1, s2]
def baz(df):
    return df.columns.tolist()

# setup 
import dask
import pandas as pd
df = pd.DataFrame({'a':[1,2,3],'b':[1,2,3],'c':[1,2,3]})

Теперь с помощью этой оболочки вы можете создать конвейер, следуя одному из следующих синтаксических шаблонов:

# wf, lt = dask_pipe(initial_var, [function_1, function_2])
# wf, lt = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})

как это:

# test 1 - lists for functions only:
workflow, last_task =  dask_pipe(df, [foo, baz])
print(dask.get(workflow, last_task)) # returns ['a','b']

# test 2 - dictionary for args:
workflow, last_task = dask_pipe(df, {foo:[], bar:['string1', 'string2']})
print(dask.get(workflow, last_task)) # returns ['a','b','string1','string2']

Ответ 11

Есть модуль dfply. Вы можете найти больше информации на

https://github.com/kieferk/dfply

Вот некоторые примеры:

from dfply import *
diamonds >> group_by('cut') >> row_slice(5)
diamonds >> distinct(X.color)
diamonds >> filter_by(X.cut == 'Ideal', X.color == 'E', X.table < 55, X.price < 500)
diamonds >> mutate(x_plus_y=X.x + X.y, y_div_z=(X.y / X.z)) >> select(columns_from('x')) >> head(3)