En R (gracias a magritrr
) ahora puede realizar operaciones con una sintaxis de tubería 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 puedes 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?
python
functional-programming
pipeline
cantdutchthis
fuente
fuente
crime_by_state %>% filter(State=="New York", Year==2005) ...
desde el final de How dplyr reemplazó mis modismos R. más comunes .Respuestas:
Una forma posible de hacer esto es utilizando un módulo llamado
macropy
. Macropy le permite aplicar transformaciones al código que ha escrito. Asía | b
se puede transformar enb(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 infijos para las funciones que está interesado en usar, simplemente marque las áreas de código en las que pretende usar 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 bytes se produce por primera vez a partir del código fuente.
Las principales desventajas son que macropy requiere una cierta forma de activarse para que funcione (mencionado más adelante). A diferencia de 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 tardará más en iniciarse. 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 tubería funcional como su sintaxis de shell emuladora 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)
fuente
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 de Pandas conserva la semántica de referencia de Python. Por eso
length_times_width
no necesita un valor de retorno; se modificax
en su lugar.fuente
PyToolz [doc] permite tuberías componibles arbitrariamente, solo que no están definidas con esa sintaxis de operador de tubería.
Siga el enlace anterior para obtener una guía de 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'
fuente
"sintaxis de canalización más funcional" ¿ es realmente una sintaxis más "funcional"? Yo diría que agrega una sintaxis "infija" a R en su lugar.
Dicho esto, la gramática de Python no tiene soporte directo para la notación infija más allá de los operadores estándar.
Si realmente necesita algo así, debe tomar ese código de Tomer Filiba como punto de partida para implementar su propia notación infija:
fuente
Si solo desea esto para scripts personales, puede considerar usar Coconut en lugar de Python.
El coco es un superconjunto de Python. Por lo tanto, puede usar el operador de tubería de Coconut
|>
, ignorando por completo el resto del lenguaje de 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)
fuente
print(1 |> isinstance(int))
... TypeError: isinstance esperaba 2 argumentos, obtuvo 1print(1 |> isinstance$(int))
, o preferiblemente,1 |> isinstance$(int) |> print
.1 |> print$(2)
llamadasprint(2, 1)
desde $ se asigna a parciales de Python. pero quieroprint(1, 2)
cuál coincide con UFCS y magrittr. Motivación:1 |> add(2) |> divide(6)
debería ser 0,5 y no debería necesitar paréntesis.1 |> isinstance$(?, int) |> print
. Para sus otros ejemplos:1 |> print$(?, 2)
,1 |> (+)$(?, 2) |> (/)$(?, 6)
. No creo que pueda evitar los paréntesis para una aplicación parcial.|>
y(+)$(?, 2)
, llegué a la conclusión de que el lenguaje de programación y el sistema matemático no quieren que use este tipo de sintaxis, y lo hace aún más feo que recurrir a un par de paréntesis. Lo usaría si tuviera una mejor sintaxis (por ejemplo, Dlang tiene UFCS pero IDK sobre funciones aritméticas, o si Python tuviera un..
operador de tubería).Hay
dfply
módulo. Puede encontrar más información enhttps://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=X.x + X.y, y_div_z=(X.y / X.z)) >> select(columns_from('x')) >> head(3)
fuente
dfply
ydplython
son los mismos paquetes. ¿Hay alguna diferencia entre ellos? @BigDataScientistdfply
,dplython
,plydata
Los paquetes son puertos pitón deldplyr
paquete, así que van a ser bastante similar en sintaxis.Me perdí el
|>
operador de tubería de Elixir, así que creé un decorador de funciones simple (~ 50 líneas de código) que reinterpreta el>>
operador de desplazamiento a la derecha de Python como una tubería muy similar a Elixir en tiempo de compilación usando la biblioteca ast y 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
Todo lo que hace es reescribir
a >> b(...)
comob(a, ...)
.https://pypi.org/project/pipeop/
https://github.com/robinhilliard/pipes
fuente
Puede utilizar la biblioteca sspipe . Expone dos objetos
p
ypx
. Similar ax %>% f(y,z)
, puedes escribirx | p(f, y, z)
y similar ax %>% .^2
puedes escribirx | px**2
.from sspipe import p, px from math import sqrt 12 | p(sqrt) | px ** 2 | p(str)
fuente
Construyendo
pipe
conInfix
Como insinuó Sylvain Leroux , podemos usar el
Infix
operador para construir un infijopipe
. Veamos cómo se logra esto.Primero, aquí está el código de Tomer Filiba
El operador de tubería pasa el objeto anterior como argumento al objeto que sigue a la tubería, por lo que
x %>% f
se puede transformar enf(x)
. En consecuencia, elpipe
operador se puede definir usandoInfix
lo siguiente: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 dedpylr
empuja los argumentos a través del primer argumento en una función, por lo quedf %>% 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 currying . La
toolz
biblioteca proporciona unacurry
función de decorador que facilita la construcción de funciones 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)
Observe que
|pipe|
empuja los argumentos a la última posición de argumento , es decirx |pipe| f(2)
corresponde a
f(2, x)
Al diseñar funciones de curry, los argumentos estáticos (es decir, los argumentos que pueden usarse para muchos ejemplos) deben colocarse antes en la lista de parámetros.
Tenga en cuenta que
toolz
incluye muchas funciones pre-curry, incluidas varias funciones deloperator
módulo.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
Usar 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, la conmutación
__or__
y__ror__
a__mod__
y__rmod__
cambiará el|
operador para elmod
operador.In [5]: 12 %pipe% sqrt %pipe% str Out[5]: '3.4641016151377544'
fuente
Añadiendo mi 2c. Yo 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 contenedora con azúcar sintáctico de estilo funcional para aplicación y composición parcial._
es un constructor estilo Scala para funciones anónimas (similar a Pythonlambda
); representa una variable, por lo que puede combinar varios_
objetos en una expresión para obtener una función con más argumentos (por ejemplo,_ + _
es equivalente alambda a, b: a + b
).F(sqrt) >> _**2 >> str
da como resultado unCallable
objeto que se puede utilizar tantas veces como desee.fuente
_
no es 100% flexible: no es compatible con todos los operadores de Python. Además, si planea usarlo_
en una sesión interactiva, debe importarlo con otro nombre (pfrom fn import _ as var
. Ej. ), Porque la mayoría (si no todos) los shells de Python interactivos usan_
para representar el último valor devuelto no asignado, sombreando así el objeto importado.No hay necesidad de bibliotecas de terceros o engaños confusos del operador para implementar una función de canalización; usted mismo puede hacer que los conceptos básicos funcionen con bastante facilidad.
Comencemos por definir qué es realmente una función de tubería. En el fondo, es solo una forma de expresar una serie de llamadas a funciones en orden lógico, en lugar del orden estándar "de adentro hacia afuera".
Por ejemplo, veamos estas funciones:
def one(value): return value def two(value): return 2*value def three(value): return 3*value
No es muy interesante, pero supongo que le están pasando cosas interesantes
value
. Queremos llamarlos en orden, pasando la salida de cada uno al siguiente. En pitón vainilla eso sería:result = three(two(one(1)))
No es increíblemente legible y, para pipelines más complejos, empeorará. Entonces, aquí hay una función de tubería simple que toma un argumento inicial y la serie de funciones para aplicarlo:
def pipe(first, *args): for fn in args: first = fn(first) return first
Vamos a llamarlo:
result = pipe(1, one, two, three)
Eso me parece una sintaxis de 'tubería' muy legible :). No veo cómo es menos legible que sobrecargar operadores o algo así. De hecho, diría que es un código Python más legible
Aquí está la humilde tubería que resuelve los ejemplos de 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)
fuente
Una solución alternativa sería utilizar la herramienta de flujo de trabajo dask. Aunque no es tan divertido sintácticamente como ...
... todavía permite que su variable fluya hacia abajo en la cadena y el uso de dask brinda el beneficio adicional de la paralelización siempre que 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 la paralelización; Si le dice a dask que obtenga una tarea en su flujo de trabajo que no depende de que otras se ejecuten primero, se ejecutarán en paralelo.
Si desea una sintaxis más sencilla, puede envolverla en algo que se encargue de nombrar 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á de acuerdo con eso, puede 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 este contenedor, 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']
fuente
Hay un
pipe
módulo muy agradable aquí https://pypi.org/project/pipe/ Se sobrecarga | operador y proporciona muchas funciones de tubería 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
Además, es muy fácil escribir sus propias funciones de tubería
@Pipe def p_sqrt(x): return sqrt(x) @Pipe def p_pr(x): print(x) 9 | p_sqrt | p_pr
fuente
La funcionalidad de la tubería se puede lograr componiendo métodos pandas con el punto. A continuación se muestra un ejemplo.
Cargue un marco de datos de muestra:
import seaborn iris = seaborn.load_dataset("iris") type(iris) # <class 'pandas.core.frame.DataFrame'>
Ilustre la composición de los métodos pandas con el punto:
(iris.query("species == 'setosa'") .sort_values("petal_width") .head())
Puede agregar nuevos métodos al marco de datos panda si es necesario (como se hace aquí, por ejemplo):
fuente