Potoki funkcjonalne w Pythonie, takie jak%>% z magrittr języka R.


87

W R (dzięki magrittr ) możesz teraz wykonywać operacje z bardziej funkcjonalną składnią potoków za pośrednictwem %>%. Oznacza to, że zamiast kodować to:

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

Możesz też to zrobić:

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

Dla mnie jest to bardziej czytelne i obejmuje przypadki użycia poza ramką danych. Czy język Python obsługuje coś podobnego?


1
Świetne pytanie. Szczególnie interesuje mnie przypadek, w którym funkcje mają więcej argumentów. Jak w crime_by_state %>% filter(State=="New York", Year==2005) ...od końca Jak dplyr zastąpić moje najczęstszych idiomów R .
Piotr Migdal

1
Oczywiście można to zrobić z wieloma lambdami, mapami i redukcjami (i jest to proste), ale zwięzłość i czytelność to główne punkty.
Piotr Migdal

13
Ten pakiet to magrittr.
piccolbo

1
Tak, z tego samego powodu, dla którego każdy pakiet R kiedykolwiek napisany był autorstwa Hadley. Jest bardziej znany. (tutaj słabo ukryty alarm zazdrości)
piccolbo

1
Zobacz odpowiedzi na stackoverflow.com/questions/33658355/ ... które rozwiązują ten problem.
Piotr Migdal

Odpowiedzi:


34

Jednym z możliwych sposobów jest użycie modułu o nazwie macropy. Makropia umożliwia zastosowanie transformacji do napisanego kodu. W ten sposób a | bmożna przekształcić w b(a). Ma to wiele zalet i wad.

W porównaniu do rozwiązania, o którym wspomniał Sylvain Leroux, Główną zaletą jest to, że nie musisz tworzyć obiektów wrostkowych dla funkcji, którymi jesteś zainteresowany - wystarczy zaznaczyć obszary kodu, które zamierzasz wykorzystać w transformacji. Po drugie, ponieważ transformacja jest stosowana w czasie kompilacji, a nie w czasie wykonywania, transformowany kod nie ponosi żadnych kosztów w czasie wykonywania - cała praca jest wykonywana, gdy kod bajtowy jest najpierw tworzony z kodu źródłowego.

Główną wadą jest to, że makropia wymaga określonego sposobu aktywacji, aby mogła działać (o czym będzie mowa później). W przeciwieństwie do szybszego środowiska uruchomieniowego, analizowanie kodu źródłowego jest bardziej złożone obliczeniowo, więc uruchomienie programu zajmie więcej czasu. Wreszcie, dodaje styl składniowy, co oznacza, że ​​programiści, którzy nie są zaznajomieni z makropią, mogą uznać twój kod za trudniejszy do zrozumienia.

Przykładowy kod:

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

I wreszcie moduł, który wykonuje ciężką pracę. Nazwałem go fpipe dla potoku funkcjonalnego jako emulująca składnia powłoki do przekazywania danych wyjściowych z jednego procesu do drugiego.

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
Brzmi świetnie, ale jak widzę działa tylko na Pythonie 2.7 (a nie na Pythonie 3.4).
Piotr Migdal

3
Stworzyłem mniejszą bibliotekę bez zależności, która robi to samo co dekorator @fpipe, ale redefiniuje prawy shift (>>) zamiast lub (|): pypi.org/project/pipeop
Robin Hilliard

odrzucony jako wymagający zewnętrznych bibliotek przy użyciu wielu dekoratorów jest bardzo złożonym rozwiązaniem dla dość prostego problemu. Dodatkowo jest to rozwiązanie tylko dla Pythona 2. Całkiem pewny, że rozwiązanie waniliowego Pythona będzie również szybsze.
jramm

40

Rury to nowa funkcja w Pandas 0.16.2 .

Przykład:

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

Uwaga: wersja Pandas zachowuje semantykę referencyjną Pythona. Dlatego length_times_widthnie potrzebuje wartości zwracanej; zmienia się xw miejscu.


4
niestety działa to tylko w przypadku ramek danych, dlatego nie mogę przypisać tego jako poprawnej odpowiedzi. ale dobrze wspomnieć tutaj, ponieważ głównym przypadkiem użycia, o którym myślałem, było zastosowanie tego do ramek danych.
cantdutchthis

22

PyToolz [doc] pozwala na dowolne komponowanie potoków, ale nie są one zdefiniowane za pomocą tej składni operatora potoku.

Skorzystaj z powyższego linku, aby uzyskać szybki start. A oto samouczek wideo: 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 to świetny wskaźnik. Powiedziawszy, że jedno łącze jest martwe, a drugie wkrótce umiera
akhmed

2
Wydaje się, że jego podstawowe adresy URL to: http://matthewrocklin.com/blog i PyToolz toolz.readthedocs.io/en/latest . Ach, efemeryczność internetu ...
smci

18

Czy język Python obsługuje coś podobnego?

„bardziej funkcjonalna składnia potoków” czy naprawdę jest to bardziej „funkcjonalna” składnia? Powiedziałbym, że zamiast tego dodaje składnię „wrostek” do R.

Mimo to gramatyka języka Python nie obsługuje bezpośrednio notacji wrostków poza standardowymi operatorami.


Jeśli naprawdę potrzebujesz czegoś takiego, powinieneś wziąć ten kod od Tomera Filiby jako punkt wyjścia do zaimplementowania własnej notacji wrostkowej:

Przykład kodu i komentarze Tomera 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)

Używając instancji tej osobliwej klasy, możemy teraz użyć nowej „składni” do wywoływania funkcji jako operatorów wrostkowych:

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

18

Jeśli chcesz tego tylko do osobistego pisania skryptów, możesz rozważyć użycie Coconut zamiast Pythona.

Coconut to nadzbiór Pythona. Możesz zatem użyć operatora potoku Coconut |>, całkowicie ignorując resztę języka Coconut.

Na przykład:

def addone(x):
    x + 1

3 |> addone

kompiluje się do

# lots of auto-generated header junk

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

def addone(x):
    return x + 1

(addone)(3)

print(1 |> isinstance(int))... TypeError: isinstance spodziewa się 2 argumentów, otrzymano 1
nyanpasu64

1
@ jimbo1qaz Jeśli nadal masz ten problem, wypróbuj print(1 |> isinstance$(int))lub najlepiej 1 |> isinstance$(int) |> print.
Solomon Ucko

@Solomon Ucko Twoja odpowiedź jest nieprawidłowa. 1 |> print$(2)wywołania, print(2, 1)ponieważ $ mapuje na części składowe Pythona. ale chcę, print(1, 2)który pasuje do UFCS i magrittr. Motywacja: 1 |> add(2) |> divide(6)powinna wynosić 0,5 i nie potrzebuję nawiasów.
nyanpasu64

@ jimbo1qaz Tak, wygląda na to, że mój poprzedni komentarz jest błędny. Naprawdę byś potrzebował 1 |> isinstance$(?, int) |> print. Inne przykłady: 1 |> print$(?, 2), 1 |> (+)$(?, 2) |> (/)$(?, 6). Nie sądzę, aby można było uniknąć nawiasów przy częściowym zastosowaniu.
Solomon Ucko

Patrząc na to, jak brzydkie jest jedno |>i drugie (+)$(?, 2), doszedłem do wniosku, że język programowania i matematyka nie chcą, żebym używał tego typu składni, i sprawia, że ​​jest to jeszcze brzydsze niż uciekanie się do zestawu nawiasów. Użyłbym go, gdyby miał lepszą składnię (np. Dlang ma UFCS, ale IDK o funkcjach arytmetycznych lub gdyby Python miał ..operator potoku).
nyanpasu64

12

Jest dfplymoduł. Więcej informacji można znaleźć pod adresem

https://github.com/kieferk/dfply

Oto kilka przykładów:

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)

Moim zdaniem to powinno być oznaczone jako poprawna. Wydaje się również, że oba dfplyi dplythonsą tymi samymi pakietami. Czy jest między nimi jakaś różnica? @BigDataScientist
InfiniteFlash

dfply, dplython, plydataPakiety są porty python z dplyrpakietu są więc będzie dość podobny w składni.
BigDataScientist

9

Brakowało mi |>operatora potoku z Elixir, więc stworzyłem prosty dekorator funkcji (~ 50 linii kodu), który reinterpretuje>> Pythona w prawo jako potok podobny do potoku Elixir w czasie kompilacji przy użyciu biblioteki ast i kompilacji / 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

Wszystko, co robi, to przepisywanie a >> b(...) jakob(a, ...) .

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

https://github.com/robinhilliard/pipes


9

Możesz użyć biblioteki sspipe . Odsłania dwa obiekty pi px. Podobnie x %>% f(y,z)jak możesz pisać x | p(f, y, z)i podobnie jak x %>% .^2potrafisz pisać x | px**2.

from sspipe import p, px
from math import sqrt

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

8

Budynek pipezInfix

Jak zasugerował Sylvain Leroux , możemy użyć Infixoperatora do skonstruowania wrostkapipe . Zobaczmy, jak to się robi.

Po pierwsze, oto kod od Tomera Filiby

Przykład kodu i komentarze Tomera 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)

Używając instancji tej osobliwej klasy, możemy teraz użyć nowej „składni” do wywoływania funkcji jako operatorów wrostkowych:

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

Operator potoku przekazuje poprzedni obiekt jako argument do obiektu, który następuje po potoku, dzięki czemu x %>% fmożna go przekształcić w f(x). W konsekwencji pipeoperator można zdefiniować Infixw następujący sposób:

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'

Uwaga dotycząca częściowego zastosowania

%>%Operatora z dpylrpopycha argumenty przez pierwszy argument funkcji, tak

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

koresponduje z

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

Najłatwiejszym sposobem osiągnięcia czegoś podobnego w Pythonie jest użycie curry . toolzBiblioteka udostępnia curryfunkcję dekoratora sprawia, że konstrukcja funkcji curried łatwe.

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)

Zauważ, że |pipe|wypycha argumenty na ostatnią pozycję argumentu , to znaczy

x |pipe| f(2)

koresponduje z

f(2, x)

Projektując funkcje curried, argumenty statyczne (tj. Argumenty, które mogą być użyte w wielu przykładach) należy umieścić wcześniej na liście parametrów.

Należy pamiętać, że toolzzawiera wiele wstępnie ustawionych funkcji, w tym różne funkcje operatormodułu.

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]

co mniej więcej odpowiada następującemu w R

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

Używanie innych ograniczników wrostków

Możesz zmienić symbole otaczające wywołanie Infix, zastępując inne metody operatora Pythona. Na przykład przełączenie __or__i __ror__do __mod__i __rmod__zmieni |operatora na modoperatora.

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

7

Do zaimplementowania funkcji potoku nie są potrzebne żadne biblioteki innych firm ani skomplikowane sztuczki operatorów - możesz łatwo uzyskać podstawy.

Zacznijmy od zdefiniowania, czym właściwie jest funkcja potoku. W istocie jest to po prostu sposób na wyrażenie serii wywołań funkcji w logicznej kolejności, a nie w standardowej kolejności „na lewą stronę”.

Na przykład spójrzmy na te funkcje:

def one(value):
  return value

def two(value):
  return 2*value

def three(value):
  return 3*value

Niezbyt interesujące, ale zakładaj, że dzieją się ciekawe rzeczy value. Chcemy wywołać je po kolei, przekazując wynik każdego z nich do następnego. W waniliowym pytonie byłoby to:

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

Nie jest niesamowicie czytelny, a dla bardziej złożonych rurociągów będzie gorzej. Oto prosta funkcja potoku, która pobiera argument początkowy i szereg funkcji, do których można go zastosować:

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

Nazwijmy to:

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

Dla mnie wygląda to na bardzo czytelną składnię potoku :). Nie widzę, żeby było to mniej czytelne niż przeciążanie operatorów lub coś w tym rodzaju. W rzeczywistości twierdziłbym, że jest to bardziej czytelny kod Pythona

Oto skromna rura rozwiązująca przykłady 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)

Bardzo podobało mi się to rozwiązanie, ponieważ składnia jest prosta i czytelna. To jest coś, co można by ciągle wpisywać. Moje jedyne pytanie brzmi, czy pętla for wpłynęłaby na wykonanie kompozycji funkcji.
Alfie González

6

Dodanie mojego 2c. Osobiście używam pakietu fn do programowania w stylu funkcjonalnym. Twój przykład przekłada się na

from fn import F, _
from math import sqrt

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

Fto klasa opakowań z funkcjonalnym cukrem syntaktycznym do częściowego zastosowania i kompozycji. _jest konstruktorem w stylu Scala dla funkcji anonimowych (podobnie jak w Pythonie lambda); reprezentuje zmienną, dlatego można połączyć kilka _obiektów w jednym wyrażeniu, aby uzyskać funkcję z większą liczbą argumentów (np. _ + _jest równoważna lambda a, b: a + b). F(sqrt) >> _**2 >> strw Callablerezultacie powstaje obiekt, którego można używać dowolną liczbę razy.


Właśnie tego szukam - nawet wspomniałem o scali jako ilustracji. Wypróbuj teraz
StephenBoesch

@javadba Cieszę się, że znalazłeś to przydatne. Zwróć uwagę, że _nie jest to w 100% elastyczne: nie obsługuje wszystkich operatorów Pythona. Dodatkowo, jeśli planujesz używać _w sesji interaktywnej, powinieneś zaimportować go pod inną nazwą (np. from fn import _ as var), Ponieważ większość (jeśli nie wszystkie) interaktywnych powłok Pythona używa _do reprezentowania ostatniej nieprzypisanej zwróconej wartości, w ten sposób cieniując zaimportowany obiekt.
Eli Korvigo,

3

Jednym z alternatywnych rozwiązań byłoby użycie dask narzędzia przepływu pracy. Chociaż nie jest to tak zabawne składniowo, jak ...

var
| do this
| then do that

... nadal umożliwia przepływ zmiennej w dół łańcucha, a użycie dask zapewnia dodatkową korzyść w postaci równoległości, jeśli to możliwe.

Oto jak używam dask, aby uzyskać wzór łańcucha rurowego:

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

Po pracy z eliksirem chciałem użyć wzorca rurociągów w Pythonie. To nie jest dokładnie ten sam wzorzec, ale jest podobny i, jak powiedziałem, ma dodatkowe zalety równoległości; jeśli powiesz dask, aby otrzymał zadanie w swoim przepływie pracy, które nie jest zależne od innych, aby uruchomiły się jako pierwsze, będą one działać równolegle.

Jeśli chcesz mieć prostszą składnię, możesz ją opakować w coś, co zajmie się nazewnictwem zadań za Ciebie. Oczywiście w takiej sytuacji wszystkie funkcje powinny przyjmować potok jako pierwszy argument i stracisz korzyści z paralizacji. Ale jeśli nie masz nic przeciwko, możesz zrobić coś takiego:

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]})

Teraz, dzięki temu opakowaniu, możesz utworzyć potok według jednego z tych wzorców składniowych:

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

lubię to:

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

Problem polega na tym, że nie możesz przekazywać funkcji jako argumentów :(
Legit Stack

3

Jest tu bardzo fajny pipemoduł https://pypi.org/project/pipe/ Przeciąża | operator i zapewnia wiele funkcji add, first, where, tailpotokowych, takich jak itp.

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

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

Dodatkowo bardzo łatwo jest napisać własne funkcje potokowe

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

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

9 | p_sqrt | p_pr

0

Funkcjonalność rury można osiągnąć, komponując metody pandy z kropką. Oto przykład poniżej.

Załaduj przykładową ramkę danych:

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

Zilustruj skład metod pandy kropką:

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

W razie potrzeby możesz dodać nowe metody do ramki danych panda (jak na przykład tutaj ):

pandas.DataFrame.new_method  = new_method

0

Moje dwa centy zainspirowane 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()

Następnie

1 |pipe| \
  (lambda x: return x+1) |pipe| \
  (lambda x: return 2*x)

zwroty

4 
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.