Functional pipes in python like %>% from R's magrittr

In R (thanks to magrittr) you can now perform operations with a more functional piping syntax via %>%. This means that instead of coding this:

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

You could also do this:

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

To me this is more readable and this extends to use cases beyond the dataframe. Does the python language have support for something similar?

59011 次浏览

Python 语言支持类似的东西吗?

“更多功能性管道语法” 这真的是一种更“功能性”的语法吗?我会说它为 R 增加了一个“ infix”语法。

也就是说,除了标准运算符之外,Python 的语法不直接支持中缀表示法。


如果你真的需要这样的东西,你应该把 Tomer Filiba 的密码作为一个起点来实现你自己的中缀表示法:

Code sample and comments by 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)

使用这个特殊类的实例,我们现在可以使用一个新的“语法” 用于作为中缀运算符调用函数:

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

一种可能的方法是使用一个名为 macropy的模块。宏使您可以将转换应用于所编写的代码。因此,a | b可以转化为 b(a)。这有许多优点和缺点。

In comparison to the solution mentioned by Sylvain Leroux, The main advantage is that you do not need to create infix objects for the functions you are interested in using -- just mark the areas of code that you intend to use the transformation. Secondly, since the transformation is applied at compile time, rather than runtime, the transformed code suffers no overhead during runtime -- all the work is done when the byte code is first produced from the source code.

主要的缺点是,宏需要以某种方式激活它才能工作(稍后将提到)。与运行速度更快的运行时相比,源代码的解析在计算上更加复杂,因此程序启动所需的时间更长。最后,它添加了一种语法样式,这意味着不熟悉宏的程序员可能会发现您的代码更难理解。

示例代码:

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

最后是完成艰苦工作的模块。我将其称为 ftube for function tube,作为它的模拟 shell 语法,用于将输出从一个进程传递到另一个进程。

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)

PyToolz [博士]允许任意组合的管道,只是它们不是用管道操作符语法定义的。

点击上面的链接,这里是一个视频教程: 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'

管道是 熊猫0.16.2的一个新特性。

例如:

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

注意: 熊猫版本保留了 Python 的参考语义。这就是为什么 length_times_width不需要返回值; 它就地修改 x

Infix建造 pipe

正如 西尔万 · 勒鲁所暗示的,我们可以使用 Infix操作符来构造中缀 pipe。让我们看看这是如何完成的。

首先,这是 Tomer Filiba的代码

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)

使用这个特殊类的实例,我们现在可以使用一个新的“语法” 用于作为中缀运算符调用函数:

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

The pipe operator passes the preceding object as an argument to the object that follows the pipe, so x %>% f can be transformed into f(x). Consequently, the pipe operator can be defined using Infix as follows:

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'

关于部分应用的一点注记

来自 dpylr%>%操作符通过函数中的第一个参数推送参数,因此

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

corresponds to

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

The easiest way to achieve something similar in Python is to use currying. The toolz library provides a curry decorator function that makes constructing curried functions easy.

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)

注意,|pipe|将参数推送到 最后一个参数位置,即

x |pipe| f(2)

相当于

f(2, x)

在设计局部函数时,静态参数(即可能用于许多示例的参数)应该放在参数列表的前面。

请注意,toolz包括许多预咖喱函数,包括来自 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]

大致相当于下面的 R

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

使用其他中缀分隔符

通过重写其他 Python 运算符方法,可以更改围绕 Infix 调用的符号。例如,将 __or____ror__切换到 __mod____rmod__将把 |操作符切换到 mod操作符。

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

如果您只是想将其用于个人脚本,那么可以考虑使用 Coconut而不是 Python。

Coconut is a superset of Python. You could therefore use Coconut's pipe operator |>, while completely ignoring the rest of the Coconut language.

例如:

def addone(x):
x + 1


3 |> addone

compiles to

# lots of auto-generated header junk


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


def addone(x):
return x + 1


(addone)(3)

添加我的2c。我个人使用包 好吧函数样式编程。你的例子翻译成

from fn import F, _
from math import sqrt


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

F是一个包装类,具有部分应用和组合的功能风格的句法代码。_是一个用于匿名函数的 Scala 风格的构造函数(类似于 Python 的 lambda) ; 它表示一个变量,因此你可以在一个表达式中组合多个 _对象来得到一个具有更多参数的函数(例如,_ + _等价于 lambda a, b: a + b)。F(sqrt) >> _**2 >> str产生一个 Callable对象,可以随意使用多次。

我忽略了 Elixir 中的 |>管道操作符,所以我创建了一个简单的函数装饰器(大约50行代码) ,它在编译时使用 ast 库和 edit/exec 将 >> Python 右移操作符重新解释为一个非常类似于 Elixir 的管道:

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

它所做的就是将 a >> b(...)重写为 b(a, ...)

Https://pypi.org/project/pipeop/

Https://github.com/robinhilliard/pipes

另一种解决方案是使用工作流工具盘。

var
| do this
| then do that

... 它仍然允许您的变量沿着链向下流动,并且使用 dask 在可能的情况下给并行化带来额外的好处。

下面是我如何使用 dask 来完成一个管道链模式:

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

在使用长生不老药之后,我想在 Python 中使用管道模式。这不是完全相同的模式,但它是相似的,就像我说的,并行化带来了额外的好处; 如果你告诉 dask 在你的工作流中获得一个不依赖于其他任务的任务,它们会并行运行。

如果您想要更简单的语法,那么您可以将它包装在一些可以为您处理任务命名的内容中。当然,在这种情况下,您需要所有函数将管道作为第一个参数,这样就会失去并行化的任何好处。但是如果你不介意的话,你可以这样做:

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

现在,使用这个包装器,您可以按照以下任何一种语法模式制作一个管道:

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

像这样:

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

可以使用 间谍库。它暴露两个对象 ppx。类似于 x %>% f(y,z),你可以写 x | p(f, y, z),类似于 x %>% .^2,你可以写 x | px**2

from sspipe import p, px
from math import sqrt


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

There is dfply module. You can find more information at

Https://github.com/kieferk/dfply

例如:

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)

这里有非常好的 pipe模块 < a href = “ https://pypi.org/project/tube/”rel = “ nofollow norefrer”> https://pypi.org/project/pipe/ 它重载 | 操作符并提供许多管道函数,如 add, first, where, tail等。

>>> [1, 2, 3, 4] | where(lambda x: x % 2 == 0) | add
6


>>> sum([1, [2, 3], 4] | traverse)
10

另外,编写自己的管道函数也很容易

@Pipe
def p_sqrt(x):
return sqrt(x)


@Pipe
def p_pr(x):
print(x)


9 | p_sqrt | p_pr

实现管道函数不需要第三方库或者混乱的操作员技巧——您可以自己轻松地掌握基本知识。

让我们从定义管道函数实际是什么开始。从本质上讲,它只是一种按逻辑顺序表示一系列函数调用的方法,而不是标准的“由内而外”顺序。

For example, lets look at these functions:

def one(value):
return value


def two(value):
return 2*value


def three(value):
return 3*value

不是很有趣,但假设有趣的事情正在发生在 value。我们希望按顺序调用它们,将每个函数的输出传递给下一个函数。在香草蟒蛇中,这个词是:

result = three(two(one(1)))

它不是难以置信的可读性,对于更复杂的管道,它会变得更糟。下面是一个简单的管道函数,它有一个初始参数,以及一系列应用它的函数:

def pipe(first, *args):
for fn in args:
first = fn(first)
return first

我们称之为:

result = pipe(1, one, two, three)

对我来说,这看起来像是非常可读的“管道”语法:)。我不认为它的可读性比重载操作符或类似的东西差。事实上,我认为它是更可读的 巨蟒代码

下面是解决 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)

管道功能可以通过组合带点的熊猫方法来实现。

加载一个示例数据帧:

import seaborn
iris = seaborn.load_dataset("iris")
type(iris)
# <class 'pandas.core.frame.DataFrame'>

Illustrate the composition of pandas methods with the dot:

(iris.query("species == 'setosa'")
.sort_values("petal_width")
.head())

如果需要,您可以向熊猫数据帧添加新的方法(例如 给你) :

pandas.DataFrame.new_method  = new_method

My two cents inspired by http://tomerfiliba.com/blog/Infix-Operators/

class FuncPipe:
class Arg:
def __init__(self, arg):
self.arg = arg
def __or__(self, func):
return func(self.arg)


def __ror__(self, arg):
return self.Arg(arg)
pipe = FuncPipe()

然后

1 |pipe| \
(lambda x: return x+1) |pipe| \
(lambda x: return 2*x)

报税表

4

Just use cool.

首先,运行 python -m pip install cool。 然后,运行 python

from cool import F


range(10) | F(filter, lambda x: x % 2) | F(sum) == 25

您可以阅读 https://github.com/abersheeran/cool以获得更多的用法。