Tubos funcionais em python como % > % do magrittr de R

em R (graças a magrittr ) pode agora efectuar operações com uma sintaxe de canalização mais funcional via %>%. Isto significa que em vez de codificar isto:

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

também podes fazer isto:

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

para mim isto é mais legível e isto estende-se a casos de uso além do dataframe. A linguagem python tem suporte para algo semelhante?

Author: cbare, 2015-01-31

15 answers

Uma maneira possível de fazer isto é usando um módulo chamado macropy. A macropia permite aplicar transformações ao código que você escreveu. Assim a | b pode ser transformado em b(a). Isto tem uma série de vantagens e desvantagens.

Em comparação com a solução mencionada por Sylvain Leroux, a principal vantagem é que você não precisa criar objetos infix para as funções que você está interessado em usar -- basta marcar as áreas de código que você pretende usar transformacao. Em segundo lugar, uma vez que a transformação é aplicada no tempo de compilação, ao invés do tempo de execução, o código transformado não sofre nenhuma sobrecarga durante o tempo de execução -- todo o trabalho é feito quando o código byte é produzido pela primeira vez a partir do código fonte.

As principais desvantagens são que a macropia requer uma certa forma de ser ativada para que ela funcione (mencionado mais tarde). Em contraste com um tempo de execução mais rápido, o processamento do código fonte é mais computacionalmente complexo e assim o programa vai levar mais tempo para começar. Finalmente, ele adiciona um estilo sintático que significa que os programadores que não estão familiarizados com a macropia podem achar seu código mais difícil de entender.

Código De Exemplo:

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']
E finalmente o módulo que faz o trabalho duro. Chamei-lhe fpipe para tubo funcional como a sintaxe do shell emulante para passar a saída de um processo para outro.

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)
 35
Author: Dunes, 2015-01-31 22:08:36

Os tubos são uma nova característica em Pandas 0.16.2 .

Exemplo:

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: a versão Pandas mantém a semântica de referência do Python. É por isso que length_times_width não precisa de um valor de retorno; ele modifica x no lugar.

 41
Author: shadowtalker, 2015-06-24 22:01:17

PyToolz [doc] permite tubos arbitrariamente compósitos, só que não são definidos com a sintaxe do operador de tubos.

Siga o link acima para o arranque rápido. E aqui está um vídeo tutorial: 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'
 23
Author: smci, 2019-01-18 04:45:22

A linguagem python tem suporte para algo semelhante?

"mais sintaxe de tubagem funcional" isto é realmente uma sintaxe mais "funcional"? Eu diria que adiciona uma sintaxe "infix" para R em vez disso.

Dito isto, a gramática de Python não tem suporte direto para a notação infix além dos operadores padrão.
Se você realmente precisa de algo assim, você deve tomar esse código de Tomer Filiba {[13] } como um ponto de partida para implementar a sua própria notação de infix:

amostra de código e observações de 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)

Usando instâncias desta classe peculiar, podemos agora usar uma nova "sintaxe" para invocar funções como operadores infix:

>>> @Infix
... def add(x, y):
...     return x + y
...
>>> 5 |add| 6
 18
Author: Sylvain Leroux, 2015-01-31 14:52:27

Se você só quer isso para scripts pessoais, você pode querer considerar usar coco em vez de Python.

O coco é um super conjunto de Python. Você poderia, portanto, usar o operador de cachimbo do coco {[[2]}, ignorando completamente o resto da linguagem de coco.

Por exemplo:

def addone(x):
    x + 1

3 |> addone

Compila até

# lots of auto-generated header junk

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

def addone(x):
    return x + 1

(addone)(3)
 18
Author: shadowtalker, 2017-10-22 07:50:15

Existe o módulo dfply. Você pode encontrar mais informações em

Https://github.com/kieferk/dfply

Alguns exemplos são:

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)
 12
Author: BigDataScientist, 2019-04-01 15:01:22

Perdi o operador de tubos |> do Elixir, por isso criei um decorador de funções simples (~ 50 linhas de código) que reinterpreta o operador de deslocamento à direita >> Python como um tubo muito semelhante ao Elixir na altura de compilação usando a biblioteca ast e 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

Tudo o que está a fazer é reescrever a >> b(...) como b(a, ...).

Https://pypi.org/project/pipeop/

Https://github.com/robinhilliard/pipes

 9
Author: Robin Hilliard, 2018-04-10 12:40:16

Pode usar a biblioteca sspipe. Expõe dois objectos p e px. Semelhante a x %>% f(y,z), você pode escrever x | p(f, y, z) e semelhante a x %>% .^2 você pode escrever x | px**2.

from sspipe import p, px
from math import sqrt

12 | p(sqrt) | px ** 2 | p(str)
 9
Author: mhsekhavat, 2018-07-20 19:43:11

Edifício pipe com Infix

Como sugerido por Sylvain Leroux, podemos usar o operador {[[12]} para construir um infix pipe. Vamos ver como isto é feito.

Primeiro, aqui está o código de Tomer Filiba.

amostra de código e observações de 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)
Usando instâncias desta classe peculiar, podemos agora usar uma nova "sintaxe" para chamar funções como operadores infix:
>>> @Infix
... def add(x, y):
...     return x + y
...
>>> 5 |add| 6

O operador do tubo passa o objecto anterior como um argumento para o objecto que segue o tubo, por isso x %>% f pode ser transformado em f(x). Consequentemente, o operador pipe pode ser definido utilizando Infix da seguinte forma:

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'

Nota de aplicação parcial

O operador %>% de dpylr empurra argumentos através do primeiro argumento numa função, por isso

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

Corresponde a

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

A maneira mais fácil de conseguir algo semelhante em Python é usaro currying . A biblioteca toolz fornece uma função decoradora curry que torna fácil a construção de funções curadas.

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)

note que |pipe| empurra os argumentos para a posição do último argumento, isso é

x |pipe| f(2)

Corresponde a

f(2, x)

Ao desenhar funções de Carriagem, argumentos estáticos (isto é, argumentos que podem ser usados para muitos exemplos) devem ser colocados mais cedo na lista de parâmetros.

Note que toolz inclui muitas funções pré-curriadas, incluindo várias funções do módulo 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]

Que corresponde aproximadamente ao seguinte em R

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

Utilização de outros delimitadores de infix

Você pode alterar os símbolos que rodeiam a invocação do Infix, anulando outros métodos do operador Python. Por exemplo, mudar __or__ e __ror__ para __mod__ e __rmod__ irá mude o operador de | para o operador de mod.

In [5]: 12 %pipe% sqrt %pipe% str
Out[5]: '3.4641016151377544'
 8
Author: yardsale8, 2017-08-21 12:56:22

Não há necessidade de bibliotecas de terceiros ou truques de operadores confusos para implementar uma função de pipe - você pode começar o básico indo muito facilmente você mesmo.

Vamos começar por definir o que é realmente uma função de pipe. Em seu coração, é apenas uma maneira de expressar uma série de chamadas de função em ordem lógica, em vez de o padrão Ordem "inside out".

Por exemplo, vejamos estas funções:

def one(value):
  return value

def two(value):
  return 2*value

def three(value):
  return 3*value
Não é muito interessante, mas suponha que coisas interessantes são ... a acontecer a value. Queremos chamá-los em ordem, passando a saída de cada um para o próximo. Em Python de baunilha que seria:
result = three(two(one(1)))
Não é incrivelmente legível e para condutas mais complexas vai piorar. Então, aqui está uma função de tubo simples que toma um argumento inicial, e a série de funções para aplicá-lo a:
def pipe(first, *args):
  for fn in args:
    first = fn(first)
  return first
Vamos chamar-lhe:
result = pipe(1, one, two, three)

Isso parece-me uma sintaxe de 'pipe' muito legível :). Não vejo como é menos legível do que sobrecarregar. operadores ou algo do género. Na verdade, eu diria que é mais legível python Código

Aqui está o humilde cachimbo que resolve os exemplos da 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)
 7
Author: jramm, 2020-04-22 06:34:35

Adicionando o meu pacote 2c. eu uso pessoalmente fn para programação de estilo funcional. O seu exemplo traduz-se em

from fn import F, _
from math import sqrt

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

F é uma classe de invólucro com açúcar sintático de estilo funcional para aplicação parcial e composição. _ é um construtor de estilo Scala para funções anónimas( semelhante ao Python's lambda); representa uma variável, pelo que pode combinar vários _ objectos numa expressão para obter uma função com mais argumentos (por exemplo _ + _ é equivalente a lambda a, b: a + b). F(sqrt) >> _**2 >> str resulta num objecto Callable que pode ser usado quantas vezes quiser.

 6
Author: Eli Korvigo, 2018-11-16 18:17:51

Uma solução alternativa seria usar a máscara da ferramenta workflow. Embora não seja tão sintaticamente divertido como...

var
| do this
| then do that
[[[6]} ... ele ainda permite que a sua variável fluir para baixo da cadeia e usando ask dá o benefício adicional de paralelização, sempre que possível.

É assim que eu uso o dask para conseguir um padrão de cadeia de tubos:

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
Depois de ter trabalhado com o elixir, quis usar o padrão de tubagem em Python. Este não é exatamente o mesmo padrão, mas é semelhante e como eu said, vem com benefícios adicionais de parallelização; se você diz a dask para obter uma tarefa em seu fluxo de trabalho que não depende de outros para correr em primeiro lugar, eles vão correr em paralelo. Se quisesse uma sintaxe mais fácil, podia embrulhá-la em algo que tratasse do nome das tarefas para si. É claro que nesta situação você precisa de todas as funções para tomar o tubo como o primeiro argumento, e você perderia qualquer benefício de paralisação. Mas se não te importares com isso podes fazer algo como isto:
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]})
Agora, com este invólucro, você pode fazer um tubo seguindo qualquer um destes padrões sintáticos:
# wf, lt = dask_pipe(initial_var, [function_1, function_2])
# wf, lt = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})
Assim:
# 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']
 3
Author: Legit Stack, 2018-07-02 17:11:14

Há um módulo muito bonito {[[2]} aqui https://pypi.org/project/pipe/ Ele sobrecarrega / operador e fornece um monte de funções de tubos como add, first, where, tail etc.

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

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

Além disso, é muito fácil escrever as próprias funções de tubos

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

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

9 | p_sqrt | p_pr
 3
Author: Dima Fomin, 2019-10-27 22:21:51

A funcionalidade do tubo pode ser alcançada compondo métodos pandas com o ponto. Aqui está um exemplo abaixo.

Carregar uma base de dados de amostra:

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

Ilustram a composição dos métodos pandas com o ponto:

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

Pode adicionar novos métodos à moldura de dados panda se necessário (como feito aqui por exemplo):

pandas.DataFrame.new_method  = new_method
 0
Author: Paul Rougieux, 2020-11-13 17:39:41
Os meus dois cêntimos inspirados em ... 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()

Depois

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

Devolve

4 
 0
Author: user3763801, 2021-01-11 01:30:19