Функциональные каналы в 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, ...)
,
Строительство 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