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?
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.
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)
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.
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
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'
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)
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)
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'
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, ...)
.
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.
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']
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)