Tubos funcionales en Python como%>% de R's magritrr


84

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?


1
Gran pregunta. Estoy especialmente interesado en el caso, donde las funciones tienen más argumentos. Como crime_by_state %>% filter(State=="New York", Year==2005) ...desde el final de How dplyr reemplazó mis modismos R. más comunes .
Piotr Migdal

1
Por supuesto, uno puede hacerlo con muchas lambdas, mapas y reduce (y es sencillo hacerlo), pero la brevedad y la legibilidad son los puntos principales.
Piotr Migdal

12
El paquete en cuestión es magrittr.
piccolbo

1
Sí, por la misma razón que todos los paquetes de R escritos fueron escritos por Hadley. Es más conocido. (alerta de envidia mal disfrazada aquí)
piccolbo

1
Vea las respuestas a stackoverflow.com/questions/33658355/… que están resolviendo este problema.
Piotr Migdal

Respuestas:


34

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 | bse puede transformar 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 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)

2
Suena genial, pero como veo, solo funciona en Python 2.7 (y no en Python 3.4).
Piotr Migdal

3
He creado una biblioteca más pequeña sin dependencias que hace lo mismo que el decorador @fpipe pero redefiniendo el desplazamiento a la derecha (>>) en lugar de o (|): pypi.org/project/pipeop
Robin Hilliard

votado a la baja por requerir bibliotecas de terceros con el uso de múltiples decoradores es una solución muy compleja para un problema bastante simple. Además, es una única solución de Python 2. Estoy bastante seguro de que la solución de vainilla pitón también será más rápida.
jramm

37

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_widthno necesita un valor de retorno; se modifica xen su lugar.


4
desafortunadamente, esto solo funciona para marcos de datos, por lo tanto, no puedo asignar esta como la respuesta correcta. pero es bueno mencionarlo aquí, ya que el caso de uso principal que tenía en mente era aplicar esto a los marcos de datos.
cantdutchthis

22

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'

1
PyToolz es un gran puntero. Habiendo dicho que un enlace está muerto y el otro
morirá

2
Sus URL base parecen ser: http://matthewrocklin.com/blog y PyToolz toolz.readthedocs.io/en/latest . Ah, lo efímero de internetz ...
smci

18

¿El lenguaje Python tiene soporte para algo similar?

"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:

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 funciones como operadores infijos:

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

18

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)

print(1 |> isinstance(int))... TypeError: isinstance esperaba 2 argumentos, obtuvo 1
nyanpasu64

1
@ jimbo1qaz Si todavía tiene este problema, intenta print(1 |> isinstance$(int)), o preferiblemente, 1 |> isinstance$(int) |> print.
Solomon Ucko

@Solomon Ucko tu respuesta es incorrecta. 1 |> print$(2)llamadas print(2, 1)desde $ se asigna a parciales de Python. pero quiero print(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.
nyanpasu64

@ jimbo1qaz Sí, parece que mi comentario anterior es incorrecto. Realmente lo necesitarías 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.
Solomon Ucko

Mirando lo feos que son ambos |>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).
nyanpasu64

11

Hay dfplymódulo. Puede 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=X.x + X.y, y_div_z=(X.y / X.z)) >> select(columns_from('x')) >> head(3)

Esto debería marcarse como la respuesta correcta, en mi opinión. Además, parece que ambos dfplyy dplythonson los mismos paquetes. ¿Hay alguna diferencia entre ellos? @BigDataScientist
InfiniteFlash

dfply, dplython, plydataLos paquetes son puertos pitón del dplyrpaquete, así que van a ser bastante similar en sintaxis.
BigDataScientist

9

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(...)como b(a, ...).

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

https://github.com/robinhilliard/pipes


9

Puede utilizar la biblioteca sspipe . Expone dos objetos py px. Similar a x %>% f(y,z), puedes escribir x | p(f, y, z)y similar a x %>% .^2puedes escribir x | px**2.

from sspipe import p, px
from math import sqrt

12 | p(sqrt) | px ** 2 | p(str)

8

Construyendo pipeconInfix

Como insinuó Sylvain Leroux , podemos usar el Infixoperador para construir un infijopipe . 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 funciones como operadores infijos:

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

El operador de tubería pasa el objeto anterior como argumento al objeto que sigue a la tubería, por lo que x %>% fse puede transformar en f(x). En consecuencia, el pipeoperador se puede definir usando Infixlo 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 de dpylrempuja los argumentos a través del primer argumento en 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 currying . La toolzbiblioteca proporciona una curryfunció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 decir

x |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 toolzincluye muchas funciones pre-curry, incluidas varias funciones del operatormó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 el modoperador.

In [5]: 12 %pipe% sqrt %pipe% str
Out[5]: '3.4641016151377544'

6

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)

Fes 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 Python lambda); 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 a lambda a, b: a + b). F(sqrt) >> _**2 >> strda como resultado un Callableobjeto que se puede utilizar tantas veces como desee.


Justo lo que estoy buscando, incluso mencioné scala como ilustración.
Probándolo

@javadba Me alegra que hayas encontrado esto útil. Tome nota, eso _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 (p from 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.
Eli Korvigo

5

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)

3

Una solución alternativa sería utilizar la herramienta de flujo de trabajo dask. Aunque no es tan divertido sintácticamente como ...

var
| do this
| then do that

... 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']

un problema con esto es que no puede pasar funciones como argumentos :(
Pila

2

Hay un pipemódulo muy agradable aquí https://pypi.org/project/pipe/ Se sobrecarga | operador y proporciona muchas funciones de tubería como, add, first, where, tailetc.

>>> [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

0

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):

pandas.DataFrame.new_method  = new_method
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.