web-dev-qa-db-fra.com

Pipes fonctionnelles en python comme%>% du magritrr de R

Dans R (grâce à magritrr), vous pouvez désormais effectuer des opérations avec une syntaxe de canalisation plus fonctionnelle via %>%. Cela signifie qu'au lieu de coder cela:

> as.Date("2014-01-01")
> as.character((sqrt(12)^2)

Vous pouvez également faire ceci:

> "2014-01-01" %>% as.Date 
> 12 %>% sqrt %>% .^2 %>% as.character

Pour moi, c'est plus lisible et cela s'étend aux cas d'utilisation au-delà de la trame de données. Le langage python prend-il en charge quelque chose de similaire?

60
cantdutchthis

Une façon possible de le faire est d'utiliser un module appelé macropy . Macropy vous permet d'appliquer des transformations au code que vous avez écrit. Ainsi a | b Peut être transformé en b(a). Cela présente un certain nombre d'avantages et d'inconvénients.

Par rapport à la solution évoquée par Sylvain Leroux, le principal avantage est que vous n'avez pas besoin de créer des objets infixes pour les fonctions que vous souhaitez utiliser - il suffit de marquer les zones de code que vous comptez utiliser pour la transformation. Deuxièmement, puisque la transformation est appliquée au moment de la compilation, plutôt qu'à l'exécution, le code transformé ne subit aucune surcharge pendant l'exécution - tout le travail est effectué lorsque le code d'octet est d'abord produit à partir du code source.

Les principaux inconvénients sont que la macropie nécessite d'être activée pour fonctionner (mentionnée plus loin). Contrairement à une exécution plus rapide, l'analyse du code source est plus complexe sur le plan informatique et le démarrage du programme prendra donc plus de temps. Enfin, il ajoute un style syntaxique qui signifie que les programmeurs qui ne sont pas familiers avec la macropie peuvent trouver votre code plus difficile à comprendre.

Exemple de code:

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

Et enfin le module qui fait le gros du travail. Je l'ai appelé fpipe pour le canal fonctionnel comme sa syntaxe d'émulation Shell pour passer la sortie d'un processus à un autre.

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)
29
Dunes

Les tuyaux sont une nouvelle fonctionnalité de Pandas 0.16.2 .

Exemple:

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 version Pandas conserve la sémantique de référence de Python. C'est pourquoi length_times_width n'a pas besoin d'une valeur de retour; il modifie x en place.

24
shadowtalker

Le langage python prend-il en charge quelque chose de similaire?

"syntaxe de tuyauterie plus fonctionnelle" est-ce vraiment une syntaxe plus "fonctionnelle"? Je dirais qu'il ajoute une syntaxe "infixe" à R à la place.

Cela étant dit, la grammaire de Python ne prend pas directement en charge la notation infixe au-delà des opérateurs standard.


Si vous avez vraiment besoin de quelque chose comme ça, vous devriez prendre ce code de Tomer Filiba comme point de départ pour implémenter votre propre notation infixe:

Échantillon de code et commentaires 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)

En utilisant des instances de cette classe particulière, nous pouvons maintenant utiliser une nouvelle "syntaxe" pour appeler des fonctions comme opérateurs infixes:

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

PyToolz[doc] autorise les tubes arbitrairement composables, mais ils ne sont pas définis avec cette syntaxe d'opérateur de pipe.

Suivez le lien ci-dessus pour le démarrage rapide. Et voici un tutoriel vidéo: 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'
16
smci

Si vous le souhaitez uniquement pour les scripts personnels, vous pouvez envisager d'utiliser Coconut au lieu de Python.

Coconut est un sur-ensemble de Python. Vous pouvez donc utiliser l'opérateur de pipe de Coconut |>, tout en ignorant complètement le reste du langage Coconut.

Par exemple:

def addone(x):
    x + 1

3 |> addone

compile en

# lots of auto-generated header junk

# Compiled Coconut: -----------------------------------------------------------

def addone(x):
    return x + 1

(addone)(3)
14
shadowtalker

Vous pouvez utiliser la bibliothèque sspipe . Il expose deux objets p et px. Semblable à x %>% f(y,z), vous pouvez écrire x | p(f, y, z) et similaire à x %>% .^2 tu peux écrire x | px**2.

from sspipe import p, px
from math import sqrt

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

Construction pipe avec Infix

Comme l'indique Sylvain Leroux , nous pouvons utiliser l'opérateur Infix pour construire un infixe pipe. Voyons comment cela est accompli.

Tout d'abord, voici le code de Tomer Filiba

Échantillon de code et commentaires 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)

En utilisant des instances de cette classe particulière, nous pouvons maintenant utiliser une nouvelle "syntaxe" pour appeler des fonctions comme opérateurs infixes:

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

L'opérateur de tuyau passe l'objet précédent comme argument à l'objet qui suit le tuyau, afin que x %>% f Puisse être transformé en f(x). Par conséquent, l'opérateur pipe peut être défini à l'aide de Infix comme suit:

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'

Une note sur l'application partielle

L'opérateur %>% De dpylr pousse les arguments dans le premier argument d'une fonction, donc

df %>% 
filter(x >= 2) %>%
mutate(y = 2*x)

correspond à

df1 <- filter(df, x >= 2)
df2 <- mutate(df1, y = 2*x)

La façon la plus simple de réaliser quelque chose de similaire dans Python est d'utiliser currying . La bibliothèque toolz fournit une fonction décoratrice curry qui rend construire des fonctions curry facilement.

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)

Notez que |pipe| Pousse les arguments dans la dernière position d'argument , c'est-à-dire

x |pipe| f(2)

correspond à

f(2, x)

Lors de la conception de fonctions curry, les arguments statiques (c'est-à-dire les arguments qui pourraient être utilisés pour de nombreux exemples) doivent être placés plus tôt dans la liste des paramètres.

Notez que toolz inclut de nombreuses fonctions pré-curry, y compris diverses fonctions du module 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]

ce qui correspond à peu près à ce qui suit dans R

> library(dplyr)
> add2 <- function(x) {x + 2}
> 0:4 %>% sapply(add2)
[1] 2 3 4 5 6

Utilisation d'autres délimiteurs d'infixes

Vous pouvez modifier les symboles qui entourent l'invocation Infixe en remplaçant les autres méthodes de l'opérateur Python. Par exemple, en changeant __or__ Et __ror__ En __mod__ et __rmod__ changera l'opérateur | en opérateur mod.

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

J'ai manqué l'opérateur de tuyau |> D'Elixir, j'ai donc créé un décorateur de fonction simple (~ 50 lignes de code) qui réinterprète l'opérateur de décalage à droite >> Python un tube très similaire à Elixir au moment de la compilation en utilisant la bibliothèque ast et 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

Il ne fait que réécrire a >> b(...) en b(a, ...).

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

https://github.com/robinhilliard/pipes

6
Robin Hilliard

Ajout de mon 2c. J'utilise personnellement le package fn pour la programmation de style fonctionnel. Votre exemple se traduit par

from fn import F, _
from math import sqrt

(F(sqrt) >> _**2 >> str)(12)

F est une classe wrapper avec du sucre syntaxique de style fonctionnel pour une application et une composition partielles. _ Est un constructeur de style Scala pour les fonctions anonymes (similaire au lambda de Python); il représente une variable, donc vous pouvez combiner plusieurs objets _ dans une expression pour obtenir une fonction avec plus d'arguments (par exemple _ + _ est équivalent à lambda a, b: a + b). F(sqrt) >> _**2 >> str donne un objet Callable qui peut être utilisé autant de fois que vous le souhaitez.

5
Eli Korvigo

Une solution alternative consisterait à utiliser l'outil de workflow. Bien que ce ne soit pas aussi amusant sur le plan syntaxique que ...

var
| do this
| then do that

... il permet toujours à votre variable de descendre dans la chaîne et l'utilisation de dask offre l'avantage supplémentaire de la parallélisation lorsque cela est possible.

Voici comment j'utilise dask pour accomplir un modèle de chaîne de tuyaux:

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

Après avoir travaillé avec elixir, j'ai voulu utiliser le modèle de passepoil en Python. Ce n'est pas exactement le même schéma, mais il est similaire et comme je l'ai dit, il présente des avantages supplémentaires de parallélisation; si vous dites à dask d'obtenir une tâche dans votre flux de travail qui ne dépend pas des autres pour s'exécuter en premier, elles s'exécuteront en parallèle.

Si vous vouliez une syntaxe plus simple, vous pourriez l'envelopper dans quelque chose qui prendrait en charge le nommage des tâches pour vous. Bien sûr, dans cette situation, vous auriez besoin de toutes les fonctions pour prendre le tuyau comme premier argument, et vous perdriez tout avantage de la parallélisation. Mais si cela vous convient, vous pouvez faire quelque chose comme ceci:

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]})

Maintenant, avec ce wrapper, vous pouvez créer un tube suivant l'un de ces modèles syntaxiques:

# wf, lt = dask_pipe(initial_var, [function_1, function_2])
# wf, lt = dask_pipe(initial_var, {function_1:[], function_2:[arg1, arg2]})

comme ça:

# 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']
2
Legit Stack

Il y a le module dfply. Vous pouvez trouver plus d'informations sur

https://github.com/kieferk/dfply

Certains exemples sont:

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)
1
BigDataScientist