Функціональні канали у пітоні, як%>% від R's magritrr


84

У R (завдяки magritrr) тепер можна виконувати операції з більш функціональним синтаксисом трубопроводів через %>%. Це означає, що замість кодування цього:

> 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 .
Piotr Migdal

1
Звичайно, це можна зробити з великою кількістю лямбда, карт і редукцій (і це просто зробити), але головні моменти - це стислість і читабельність.
Piotr Migdal

12
Розглянута упаковка - magrittr.
piccolbo

1
Так, з тієї ж причини кожен написаний коли-небудь пакет R був написаний Хедлі. Він більш відомий. (тут погано замаскована заздрість)
пікколбо

1
Див. Відповіді на stackoverflow.com/questions/33658355/… , які вирішують цю проблему.
Piotr Migdal

Відповіді:


34

Одним із можливих способів цього є використання модуля з назвою 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).
Piotr Migdal

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

проголосовано як вимагання сторонніх бібліотек із використанням декількох декораторів є дуже складним рішенням для досить простої проблеми. Плюс це рішення лише для python 2. Практично впевнений, що розчин ванільного пітону також буде швидшим.
jramm

37

Труби - це нова функція в 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

Примітка: Версія Pandas зберігає посилальну семантику Python. Ось чому length_times_widthне потрібно повертається значення; він модифікується xна місці.


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

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 . Ах,
швидкоплинність

18

Чи підтримує мова python щось подібне?

"більш функціональний синтаксис трубопроводів" це насправді більш "функціональний" синтаксис? Я б сказав, що натомість до R додається синтаксис "infix".

З огляду на це, граматика 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

Якщо ви просто хочете це зробити для особистих сценаріїв, ви можете розглянути можливість використання кокосового горіха замість 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)

print(1 |> isinstance(int))... TypeError: isinstance очікується 2 аргументи, отримано 1
nyanpasu64

1
@ Jimbo1qaz Якщо у вас є така проблема, спробуйте print(1 |> isinstance$(int)), або переважно 1 |> isinstance$(int) |> print.
Соломон Уцко

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

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

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

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)

На мою думку, це повинно бути позначено як правильну відповідь. Крім того, схоже, що і те, dfplyі інше dplython- це однакові пакунки. Чи є якась різниця між ними? @BigDataScientist
InfiniteFlash

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

9

Я пропустив |>оператор pipe від 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


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'

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 використовують _для представлення останнього непризначеного поверненого значення, тим самим затінюючи імпортований об'єкт.
Елі Корвіго

6

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

Почнемо з визначення того, що насправді є функцією труби. В його основі - це лише спосіб виразити низку викликів функцій у логічному порядку, а не в стандартному порядку „навиворіт”.

Наприклад, давайте розглянемо ці функції:

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)

Це здається мені дуже читабельним синтаксисом "pipe" :). Я не бачу, як це є менш читабельним, ніж перевантаження операторів чи щось подібне. Насправді я б стверджував, що це більш читабельний код python

Ось скромна труба, яка вирішує приклади ОП:

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)

3

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

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']

одна проблема з цим полягає в тому, що ви не можете передавати функції як аргументи :(
Legit Stack

2

Тут є дуже приємний pipeмодуль https://pypi.org/project/pipe/ Він перевантажує | оператора та надає безліч функцій 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

Функціональність труби може бути досягнута складанням методів панд з крапкою. Ось приклад нижче.

Завантажте зразок кадру даних:

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

Проілюструйте склад методів панд крапкою:

(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.