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?
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.
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)
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.
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'
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
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)
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)
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, ...)
.
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)
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.
amostra de código e observações de Tomer Filiba ( http://tomerfiliba.com/blog/Infix-Operators/) :
Usando instâncias desta classe peculiar, podemos agora usar uma nova "sintaxe" para chamar funções como operadores infix: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
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'
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)
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.
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']
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
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
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