dfply - Tubos funcionales en python como%>% de R dplyr
functional-programming pipeline (10)
En R (gracias a dplyr
) ahora puede realizar operaciones con una sintaxis de canalización más funcional a través de %>%
. Esto significa que en lugar de codificar esto:
> as.Date("2014-01-01")
> as.character((sqrt(12)^2)
También podrías hacer esto:
> "2014-01-01" %>% as.Date
> 12 %>% sqrt %>% .^2 %>% as.character
Para mí, esto es más legible y se extiende a casos de uso más allá del marco de datos. ¿El lenguaje python tiene soporte para algo similar?
¿El lenguaje python tiene soporte para algo similar?
La "sintaxis de canalización más funcional" ¿ es realmente una sintaxis más "funcional"? Yo diría que agrega una sintaxis de "infijo" a R en su lugar.
Dicho esto, la gramática de Python no tiene soporte directo para la notación infijo más allá de los operadores estándar.
Si realmente necesitas algo así, deberías tomar ese código de Tomer Filiba como punto de partida para implementar tu propia notación de infijo:
Ejemplo de código y comentarios 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 instancias de esta clase peculiar, ahora podemos usar una nueva "sintaxis" para llamar a las funciones como operadores de infijo:
>>> @Infix ... def add(x, y): ... return x + y ... >>> 5 |add| 6
Añadiendo mi 2c. Personalmente uso el paquete fn para la programación de estilo funcional. Tu ejemplo se traduce en
from fn import F, _
from math import sqrt
(F(sqrt) >> _**2 >> str)(12)
F
es una clase de envoltura con azúcar sintáctica de estilo funcional para aplicación y composición parcial. _
es un constructor de estilo Scala para funciones anónimas (similar a la lambda
de Python); representa una variable, por lo tanto, puede combinar varios objetos _
en una expresión para obtener una función con más argumentos (por ejemplo, _ + _
es equivalente a lambda a, b: a + b
). F(sqrt) >> _**2 >> str
da como resultado un objeto Callable
que puede usarse tantas veces como desee.
Extrañé al operador de tuberías |>
de Elixir, así que creé una función simple decorator (~ 50 líneas de código) que reinterpreta el operador de cambio a la derecha de >>
Python como una tubería muy similar a Elixir en tiempo de compilación usando la biblioteca ast y la compilación / 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
Todo lo que está haciendo es reescribir a >> b(...)
como b(a, ...)
.
Las tuberías son una nueva característica en Pandas 0.16.2 .
Ejemplo:
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: La versión Pandas conserva la semántica de referencia de Python. Es por eso que length_times_width
no necesita un valor de retorno; modifica x
en su lugar.
Puede utilizar la biblioteca sspipe . Expone dos objetos p
y px
. Similar a x %>% f(y,z)
, puede escribir x | p(f, y, z)
x | p(f, y, z)
y similar a x %>% .^2
puede escribir x | px**2
x | px**2
.
from sspipe import p, px
from math import sqrt
12 | p(sqrt) | px ** 2 | p(str)
Si solo quieres esto para los scripts personales, puedes considerar usar Coconut lugar de Python.
El coco es un superconjunto de Python. Por lo tanto, puedes usar el operador de tuberías de Coconut, ignorando completamente el resto del lenguaje Coconut.
Por ejemplo:
def addone(x):
x + 1
3 |> addone
compila a
# lots of auto-generated header junk
# Compiled Coconut: -----------------------------------------------------------
def addone(x):
return x + 1
(addone)(3)
Una posible forma de hacerlo es mediante el uso de un módulo llamado macropy
. Macropy te permite aplicar transformaciones al código que has escrito. Por lo tanto a | b
a | b
puede transformarse en b(a)
. Esto tiene una serie de ventajas y desventajas.
En comparación con la solución mencionada por Sylvain Leroux, la principal ventaja es que no necesita crear objetos de infijo para las funciones que está interesado en usar, solo marque las áreas de código que pretende usar en la transformación. En segundo lugar, dado que la transformación se aplica en tiempo de compilación, en lugar de en tiempo de ejecución, el código transformado no sufre sobrecarga durante el tiempo de ejecución: todo el trabajo se realiza cuando el código de byte se produce por primera vez a partir del código fuente.
Las principales desventajas son que la macropía requiere una cierta forma de activación para que funcione (se menciona más adelante). En contraste con un tiempo de ejecución más rápido, el análisis del código fuente es más complejo computacionalmente y, por lo tanto, el programa demorará más en comenzar. Finalmente, agrega un estilo sintáctico que significa que los programadores que no están familiarizados con macropy pueden encontrar su código más difícil de entender.
Código de ejemplo:
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'']
Y finalmente el módulo que hace el trabajo duro. Lo he llamado fpipe para canalización funcional como su sintaxis de shell emulada para pasar la salida de un proceso a otro.
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)
Una solución alternativa sería utilizar la herramienta de flujo de trabajo dask. Aunque no es tan sintácticamente divertido como ...
var
| do this
| then do that
... todavía permite que su variable fluya hacia abajo de la cadena y el uso de dask brinda el beneficio adicional de paralelización cuando sea posible.
Así es como uso dask para lograr un patrón de cadena de tuberías:
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
Después de haber trabajado con elixir, quería usar el patrón de tuberías en Python. Este no es exactamente el mismo patrón, pero es similar y, como dije, viene con beneficios adicionales de paralelización; Si le dice a dask que obtenga una tarea en su flujo de trabajo que no depende de que otros ejecuten primero, se ejecutarán en paralelo.
Si quisiera una sintaxis más sencilla, podría envolverla en algo que se ocupe de la asignación de nombres de las tareas por usted. Por supuesto, en esta situación, necesitaría todas las funciones para tomar la tubería como primer argumento y perdería cualquier beneficio de la paralización. Pero si estás de acuerdo con eso, podrías hacer algo como esto:
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]})
Ahora, con esta envoltura, puede hacer una tubería siguiendo cualquiera de estos patrones sintácticos:
# wf, lt = dask_pipe(initial_var, [function_1, function_2])
# wf, lt = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})
Me gusta esto:
# 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'']
PyToolz permite canalizaciones de PyToolz arbitraria, pero no están definidas con esa sintaxis de operador de tubería.
Siga el enlace de arriba para el inicio rápido. Y aquí hay un video 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''
pipe
construcción con Infix
Como lo insinuó Sylvain Leroux , podemos usar el operador de Infix
para construir una pipe
infijo. Veamos cómo se logra esto.
Primero, aquí está el código de Tomer Filiba.
Ejemplo de código y comentarios 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 instancias de esta clase peculiar, ahora podemos usar una nueva "sintaxis" para llamar a las funciones como operadores de infijo:
>>> @Infix ... def add(x, y): ... return x + y ... >>> 5 |add| 6
El operador de tubería pasa el objeto anterior como un argumento al objeto que sigue a la tubería, por lo que x %>% f
se puede transformar en f(x)
. En consecuencia, el operador de pipe
se puede definir usando Infix
siguiente manera:
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''
Una nota sobre aplicación parcial.
El operador %>%
de dpylr
argumentos en el primer argumento de una función, por lo que
df %>%
filter(x >= 2) %>%
mutate(y = 2*x)
corresponde a
df1 <- filter(df, x >= 2)
df2 <- mutate(df1, y = 2*x)
La forma más fácil de lograr algo similar en Python es usar el currying . La biblioteca de toolz
proporciona una función de decoración de curry
que facilita la construcción de funciones de 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)
Tenga en cuenta que |pipe|
inserta los argumentos en la posición del último argumento , es decir
x |pipe| f(2)
corresponde a
f(2, x)
Al diseñar funciones con curry, los argumentos estáticos (es decir, los argumentos que podrían usarse para muchos ejemplos) deben ubicarse antes en la lista de parámetros.
Tenga en cuenta que toolz
incluye muchas funciones previamente toolz
, incluidas varias funciones del módulo del 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 a lo siguiente en R
> library(dplyr)
> add2 <- function(x) {x + 2}
> 0:4 %>% sapply(add2)
[1] 2 3 4 5 6
Usando otros delimitadores de infijo
Puede cambiar los símbolos que rodean la invocación de Infix anulando otros métodos de operador de Python. Por ejemplo, cambiar __or__
y __ror__
a __mod__
y __rmod__
cambiará el |
Operador al operador mod
.
In [5]: 12 %pipe% sqrt %pipe% str
Out[5]: ''3.4641016151377544''