Tubos funcionales en python como%>% de R’s dplyr

En R (gracias a dplyr ) ahora puede realizar operaciones con una syntax 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 dataframe. ¿El lenguaje python tiene soporte para algo similar?

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 comstackció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 progtwig demorará más en comenzar. Finalmente, agrega un estilo sintáctico que significa que los progtwigdores 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 'abc' | 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 syntax 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) 

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.

¿El lenguaje python tiene soporte para algo similar?

La “syntax de canalización más funcional” ¿ es realmente una syntax más “funcional”? Yo diría que agrega una syntax 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 “syntax” para llamar a las funciones como operadores de infijo:

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

PyToolz [doc] permite canalizaciones de composición arbitraria, pero no están definidas con esa syntax 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' 

Si solo quieres esto para las secuencias de comandos personales, puedes considerar usar Coconut en 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 rest del lenguaje Coconut.

Por ejemplo:

 def addone(x): x + 1 3 |> addone 

comstack a

 # lots of auto-generated header junk # Compiled Coconut: ----------------------------------------------------------- def addone(x): return x + 1 (addone)(3) 

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 “syntax” 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 utilizando 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 la 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 curry . 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 al curry, los argumentos estáticos (es decir, los argumentos que se pueden usar para muchos ejemplos) deben ubicarse antes en la lista de parámetros.

Tenga en cuenta que toolz incluye muchas funciones predefinidas, 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' 

Eché de menos 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 comstackción utilizando la biblioteca ast y la comstackció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, ...) .

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

https://github.com/robinhilliard/pipes

Añadiendo mi 2c. Personalmente uso el paquete fn para la progtwigció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.

Puedes usar la librería 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) 

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 

… aún 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 syntax 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'] 

Hay módulo dfply . Puedes encontrar más información en

https://github.com/kieferk/dfply

Algunos ejemplos son:

 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=Xx + Xy, y_div_z=(Xy / Xz)) >> select(columns_from('x')) >> head(3)