Функциональные каналы в Python, такие как%>% от R magrittr


87

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

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

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

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

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


1
Отличный вопрос. Меня особенно интересует случай, когда у функций больше аргументов. Как и в crime_by_state %>% filter(State=="New York", Year==2005) ...с конца Как dplyr заменить мои наиболее распространенные идиомы R .
Петр Мигдал

1
Конечно, это можно сделать с большим количеством лямбд, карт и сокращений (а это просто сделать), но краткость и удобочитаемость являются основными моментами.
Петр Мигдал

13
Речь идет о пакете magrittr.
пикколбо

1
Да, по той же причине, по которой каждый когда-либо написанный пакет R был автором Хэдли. Он более известен. (здесь оповещение о плохо замаскированной зависти)
пикколбо

1
См. Ответы на stackoverflow.com/questions/33658355/… , которые решают эту проблему.
Петр Мигдал,

Ответы:


34

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

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

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

Пример кода:

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
Звучит здорово, но, как я вижу, он работает только на Python 2.7 (но не на Python 3.4).
Петр Мигдал

3
Я создал меньшую библиотеку без зависимостей, которая делает то же самое, что и декоратор @fpipe, но переопределяет сдвиг вправо (>>) вместо или (|): pypi.org/project/pipeop
Робин Хиллиард,

отвергнутый как требующий сторонних библиотек с использованием нескольких декораторов, является очень сложным решением довольно простой проблемы. Кроме того, это решение только для Python 2. Уверен, что решение с ванильным питоном тоже будет быстрее.
jramm

40

Каналы - это новая функция в 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на месте.


4
К сожалению, это работает только для фреймов данных, поэтому я не могу назначить это правильным ответом. но хорошо упомянуть здесь, поскольку основной вариант использования, который я имел в виду, - это применить это к фреймам данных.
cantdutchthis

22

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'

1
PyToolz - отличный указатель. Сказав, что одно звено мертво, а другое скоро умирает
Ахмед

2
Его базовые URL-адреса выглядят так: http://matthewrocklin.com/blog и PyToolz toolz.readthedocs.io/en/latest . Ах, эфемерность internetz ...
smci

18

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

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

При этом грамматика Python не имеет прямой поддержки инфиксной нотации, кроме стандартных операторов.


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

Пример кода и комментарии Томера Филиба ( 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

18

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

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

Например:

def addone(x):
    x + 1

3 |> addone

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

# lots of auto-generated header junk

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

def addone(x):
    return x + 1

(addone)(3)

print(1 |> isinstance(int))... TypeError: isinstance ожидал 2 аргумента, получил 1
nyanpasu64

1
@ jimbo1qaz Если у вас все еще есть эта проблема, попробуйте print(1 |> isinstance$(int)), а лучше 1 |> isinstance$(int) |> print.
Соломон Учко

@ Соломон Уко, ваш ответ неверен. 1 |> print$(2)вызывает, print(2, 1)так как $ отображается на части Python. но я хочу, print(1, 2)что соответствует UFCS и magrittr. Мотивация: 1 |> add(2) |> divide(6)должно быть 0,5, скобки не нужны.
nyanpasu64 08

@ jimbo1qaz Да, похоже, мой предыдущий комментарий неверен. Вам действительно понадобится 1 |> isinstance$(?, int) |> print. Для других ваших примеров: 1 |> print$(?, 2), 1 |> (+)$(?, 2) |> (/)$(?, 6). Я не думаю, что вы можете избежать скобок для частичного применения.
Соломон Учко

Глядя на то, как уродливы оба |>и(+)$(?, 2) , я пришел к выводу, что истеблишмент языков программирования и математики не хочет, чтобы я использовал этот тип синтаксиса, и делает его даже более уродливым, чем использование набора круглых скобок. Я бы использовал его, если бы у него был лучший синтаксис (например, у Dlang есть UFCS, но у IDK есть арифметические функции, или если бы у Python был ..оператор канала).
nyanpasu64 08

12

Есть 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)

На мой взгляд, это следует отметить как правильный ответ. Кроме того, похоже, что оба dfplyи dplythonявляются одними и теми же пакетами. Есть ли между ними разница? @BigDataScientist
InfiniteFlash

dfply, dplython, plydataПакеты питона порты dplyrпакет , так что они будут очень похожи на синтаксис.
BigDataScientist

9

Я пропустил |>оператор канала из 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


9

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

from sspipe import p, px
from math import sqrt

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

8

Строительство 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

Нет необходимости в сторонних библиотеках или запутанных уловках операторов для реализации функции конвейера - вы можете довольно легко освоить основы самостоятельно.

Начнем с определения того, что на самом деле представляет собой функция канала. По сути, это просто способ выразить последовательность вызовов функций в логическом порядке, а не в стандартном порядке «наизнанку».

Например, давайте посмотрим на эти функции:

def one(value):
  return value

def two(value):
  return 2*value

def three(value):
  return 3*value

Не очень интересно, но предполагаю, что интересные вещи происходят value. Мы хотим вызывать их по порядку, передавая вывод каждого следующему. В ванильном питоне это будет:

result = three(two(one(1)))

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

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

Назовем это:

result = pipe(1, one, two, three)

Мне это кажется очень читаемым синтаксисом "трубы" :). Я не понимаю, почему это менее читаемо, чем операторы перегрузки или что-то в этом роде. На самом деле, я бы сказал, что это более читаемый код Python.

Вот скромная труба, решающая примеры OP:

from math import sqrt
from datetime import datetime

def as_date(s):
  return datetime.strptime(s, '%Y-%m-%d')

def as_character(value):
  # Do whatever as.character does
  return value

pipe("2014-01-01", as_date)
pipe(12, sqrt, lambda x: x**2, as_character)

6

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

from fn import F, _
from math import sqrt

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

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


Именно то, что я ищу - даже упомянул scala в качестве иллюстрации.
Пробуем прямо

@javadba Я рад, что вы нашли это полезным. Обратите внимание, что _это не на 100% гибкое: он не поддерживает все операторы Python. Кроме того, если вы планируете использовать _в интерактивном сеансе, вы должны импортировать его под другим именем (например from fn import _ as var), потому что большинство (если не все) интерактивные оболочки Python используют _для представления последнего неназначенного возвращаемого значения, тем самым затеняя импортированный объект.
Эли Корвиго

3

Одним из альтернативных решений может быть использование инструмента рабочего процесса 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

После работы с elixir я захотел использовать паттерн трубопроводов в 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']

одна проблема в том, что вы не можете передавать функции в качестве аргументов :(
Legit Stack

3

Здесь очень хороший pipeмодуль https://pypi.org/project/pipe/ Он перегружает | оператор и предоставляет множество функций конвейера, таких как add, first, where, tailи т. д.

>>> [1, 2, 3, 4] | where(lambda x: x % 2 == 0) | add
6

>>> sum([1, [2, 3], 4] | traverse)
10

Плюс очень легко писать собственные пайп-функции

@Pipe
def p_sqrt(x):
    return sqrt(x)

@Pipe
def p_pr(x):
    print(x)

9 | p_sqrt | p_pr

0

Функциональность канала может быть достигнута путем компоновки методов pandas с точкой. Вот пример ниже.

Загрузите образец фрейма данных:

import seaborn    
iris = seaborn.load_dataset("iris")
type(iris)
# <class 'pandas.core.frame.DataFrame'>

Проиллюстрируйте состав методов pandas точкой:

(iris.query("species == 'setosa'")
     .sort_values("petal_width")
     .head())

При необходимости вы можете добавить новые методы во фрейм данных panda (например, как это сделано здесь ):

pandas.DataFrame.new_method  = new_method
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.