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

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

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

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

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

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

16 ответов

Решение

Одним из возможных способов сделать это является использование модуля под названием 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)

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

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

Перейдите по ссылке выше для быстрого запуска. А вот видеоурок: 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'

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

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

Например:

def addone(x):
    x + 1

3 |> addone

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

# lots of auto-generated header junk

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

def addone(x):
    return x + 1

(addone)(3)

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

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

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


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

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

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

Вы можете использовать библиотеку 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)

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

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

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

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)

Я пропустил |> оператор канала из Elixir, поэтому я создал простой декоратор функций (~ 50 строк кода), который интерпретирует >> Оператор правого сдвига Python как очень похожий на эликсир канал во время компиляции с использованием библиотеки 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

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

Добавление моего 2с. Я лично использую пакет 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 объект, который можно использовать столько раз, сколько вы хотите.

Там очень красиво 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

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

Функциональность канала может быть достигнута путем компоновки методов 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

Просто используйте cool.

Первый забег python -m pip install cool. Затем запустите python.

      from cool import F

range(10) | F(filter, lambda x: x % 2) | F(sum) == 25

Вы можете прочитать https://github.com/abersheeran/cool, чтобы узнать больше об использовании.

Мои два цента вдохновлены 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 
Другие вопросы по тегам