【问题标题】:Functional pipes in python like %>% from R's magrittrpython中的功能管道,如来自R's magrittr的%>%
【发布时间】:2015-03-30 21:56:31
【问题描述】:

在 R 中(感谢 magrittr),您现在可以通过 %>% 使用更实用的管道语法执行操作。这意味着不用编码:

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

你也可以这样做:

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

对我来说,这更具可读性,并且可以扩展到数据框之外的用例。 python语言是否支持类似的东西?

【问题讨论】:

  • 好问题。我对函数有更多参数的情况特别感兴趣。与crime_by_state %>% filter(State=="New York", Year==2005) ... 中的How dplyr replaced my most common R idioms 一样。
  • 当然,可以使用大量 lambda、map 和 reduce 来实现(而且这样做很简单),但简洁和可读性是重点。
  • 有问题的包是 magrittr。
  • 是的,出于同样的原因,曾经编写的每个 R 包都是由 Hadley 编写的。他的知名度更高。 (这里伪装得很嫉妒警告)
  • 查看解决此问题的stackoverflow.com/questions/33658355/… 的答案。

标签: python functional-programming pipeline


【解决方案1】:

只需使用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以获取更多用法。

【讨论】:

    【解决方案2】:

    我的两分钱灵感来自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 
    

    【讨论】:

      【解决方案3】:

      管道功能可以通过用点组成pandas方法来实现。下面是一个例子。

      加载示例数据框:

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

      用圆点说明 pandas 方法的组成:

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

      如果需要,您可以向 panda 数据框添加新方法(例如 here):

      pandas.DataFrame.new_method  = new_method
      

      【讨论】:

        【解决方案4】:

        不需要 3rd 方库或令人困惑的操作符来实现管道功能 - 您可以自己轻松掌握基础知识。

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

        例如,让我们看看这些函数:

        def one(value):
          return value
        
        def two(value):
          return 2*value
        
        def three(value):
          return 3*value
        

        不是很有趣,但假设value 正在发生有趣的事情。我们想按顺序调用它们,将每个的输出传递给下一个。在 vanilla python 中,这将是:

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

        它的可读性并不高,对于更复杂的管道,它会变得更糟。因此,这里有一个简单的管道函数,它接受一个初始参数,以及将其应用于的一系列函数:

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

        我们称之为:

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

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

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

        【讨论】:

        • 我非常喜欢这个解决方案,因为它的语法简单易读。这是一个可以不断输入的东西。我唯一的问题是 for 循环是否会影响函数组合的性能。
        • Python3 需要在代码中添加list(list(list(... )))。几乎无法阅读。倒读也是这样。试试fluentpyinfixpy
        • @StephenBoesch....在这两个方面都错了。我不明白为什么您认为有必要多次致电list?这里的解决方案根本不返回列表。同样,pipe 函数不会向后读取 - 它是从左到右的。如果你想从右到左,你正在寻找一个compose 函数。使用相同的原则同样容易做到
        • 这当然不是“两方面都错”。 python3 默认为迭代器,因此需要list 来实现集合的结果。多个mapfilter 等每个都需要一个list,因此会污染代码。 pipe 是第三方库,因此您的评论不适用于它
        【解决方案5】:

        这里有很好的pipe 模块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
        

        【讨论】:

          【解决方案6】:

          dfply 模块。您可以在

          找到更多信息

          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)
          

          【讨论】:

          • 在我看来,这应该被标记为正确答案。此外,dfplydplython 似乎都是相同的包。它们之间有什么区别吗? @BigDataScientist
          • dfply, dplython, plydata 包是 dplyr 包的 python 端口,因此它们在语法上将非常相似。
          • dfply 是唯一一个最近被远程触及的:即使是那个,截至 2021 年 3 月的 18 个月内已关闭的问题或提交为零。我对项目进行了 ping 操作,看看他们是否有任何计划“醒来”
          • FWIW 我维护了另一个名为 siuba 的端口。它具有能够生成 SQL 代码和加速分组操作的额外优势! github.com/machow/siuba
          • 不可能不是正确答案。 3 &gt;&gt; np.sqrt 给出错误,但在 R 中是 3 %&gt;% sqrt
          【解决方案7】:

          PyToolz [doc] 允许任意组合管道,只是它们没有使用管道运算符语法定义。

          点击上面的链接获取快速入门。这是一个视频教程: 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'
          

          【讨论】:

          • PyToolz 是一个很好的指针。话虽如此,一个链接已死,另一个链接很快就会死
          • 他的基本 URL 似乎是:http://matthewrocklin.com/blog 和 PyToolz toolz.readthedocs.io/en/latest。啊,互联网的短暂性......
          • 糟糕的是你不能做多参数函数
          • @Frank:嘿伙计,这个开源软件,作者不会从你我那里得到报酬,所以不要说“包 X 很烂”,而只是说“包 X 仅限使用- case Y',和/或建议更好的替代包,或将该功能贡献给包 X,或自己编写。
          • sspipe,如下所述,效果非常好。另外,我不是说包装很烂,我说缺少某些功能很烂。
          【解决方案8】:

          添加我的 2c。我个人使用包fn 进行函数式编程。你的例子翻译成

          from fn import F, _
          from math import sqrt
          
          (F(sqrt) >> _**2 >> str)(12)
          

          F 是一个包装类,带有用于部分应用和组合的函数式语法糖。 _ 是 Scala 风格的匿名函数构造函数(类似于 Python 的 lambda);它代表一个变量,因此您可以在一个表达式中组合多个_ 对象以获得具有更多参数的函数(例如_ + _ 相当于lambda a, b: a + b)。 F(sqrt) &gt;&gt; _**2 &gt;&gt; str 产生一个 Callable 对象,可以根据需要多次使用。

          【讨论】:

          • 正是我想要的——甚至提到了 scala 作为插图。马上试用
          • @javadba 很高兴您发现这很有用。请注意,_ 并非 100% 灵活:它不支持所有 Python 运算符。另外,如果您打算在交互式会话中使用_,您应该以另一个名称(例如from fn import _ as var)导入它,因为大多数(如果不是全部)交互式Python shell 使用_ 来表示最后一个未分配的返回值,从而遮蔽导入的对象。
          【解决方案9】:

          您可以使用sspipe 库。它公开了两个对象ppx。和x %&gt;% f(y,z)类似,可以写成x | p(f, y, z),和x %&gt;% .^2类似,可以写成x | px**2

          from sspipe import p, px
          from math import sqrt
          
          12 | p(sqrt) | px ** 2 | p(str)
          

          【讨论】:

          • 这看起来不错,但你能通过管道传递给第二个变量 3 | p( f( x, . ) ) 吗?在 R 中,这将是:3 %&gt;% f(x, .)
          • @Frank 是的。这可以通过px 实现,它类似于R 中的.。不过,您应该注意px 应该传递给p(),而不是f()。示例:3 | p(f, x, px)
          【解决方案10】:

          另一种解决方案是使用工作流工具 dask。虽然它在语法上不如...

          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
          

          在使用了 elixir 之后,我想在 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']
          

          【讨论】:

          • 这样做的一个问题是您不能将函数作为参数传递:(
          【解决方案11】:

          我错过了 Elixir 中的 |&gt; 管道运算符,因此我创建了一个简单的函数装饰器(大约 50 行代码),它在编译时使用 ast 将 &gt;&gt; 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 &gt;&gt; b(...) 重写为b(a, ...)

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

          https://github.com/robinhilliard/pipes

          【讨论】:

            【解决方案12】:

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

            Coconut 是 Python 的超集。因此,您可以使用 Coconut 的管道运算符 |&gt;,而完全忽略 Coconut 语言的其余部分。

            例如:

            def addone(x):
                x + 1
            
            3 |> addone
            

            编译成

            # lots of auto-generated header junk
            
            # Compiled Coconut: -----------------------------------------------------------
            
            def addone(x):
                return x + 1
            
            (addone)(3)
            

            【讨论】:

            • print(1 |&gt; isinstance(int))... TypeError: isinstance 预期 2 个参数,得到 1 个
            • @jimbo1qaz 如果你仍然有这个问题,试试print(1 |&gt; isinstance$(int)),或者最好是1 |&gt; isinstance$(int) |&gt; print
            • @Solomon Ucko 你的答案是错误的。 1 |&gt; print$(2) 调用 print(2, 1) 因为 $ 映射到 Python 部分。但我想要匹配 UFCS 和 magrittr 的 print(1, 2)。动机:1 |&gt; add(2) |&gt; divide(6) 应该是 0.5,我不需要括号。
            • @jimbo1qaz 是的,看来我之前的评论是错误的。你实际上需要1 |&gt; isinstance$(?, int) |&gt; print。对于您的其他示例:1 |&gt; print$(?, 2)1 |&gt; (+)$(?, 2) |&gt; (/)$(?, 6)。我认为你不能避免部分应用的括号。
            • 看看|&gt;(+)$(?, 2) 的丑陋程度,我得出的结论是编程语言和数学机构不希望我使用这种类型的语法,并且使它甚至比使用一组括号还要丑陋。如果它有更好的语法,我会使用它(例如,Dlang 有 UFCS 但关于算术函数的 IDK,或者 Python 有 .. 管道运算符)。
            【解决方案13】:

            建设pipeInfix

            正如Sylvain Leroux 所暗示的,我们可以使用Infix 运算符来构造一个中缀pipe。让我们看看这是如何实现的。

            首先,这是来自Tomer Filiba的代码

            Tomer Filiba (http://tomerfiliba.com/blog/Infix-Operators/) 的代码示例和 cmets:

            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
            

            管道运算符将前面的对象作为参数传递给管道后面的对象,因此x %&gt;% f 可以转换为f(x)。因此,pipe 运算符可以使用Infix 定义如下:

            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 中的 %&gt;% 运算符将参数推入函数中的第一个参数,所以

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

            对应于

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

            在 Python 中实现类似功能的最简单方法是使用 curryingtoolz 库提供了一个 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)
            

            注意|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 运算符方法来更改围绕中缀调用的符号。例如,将__or____ror__ 切换为__mod____rmod__ 会将| 运算符更改为mod 运算符。

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

            【讨论】:

              【解决方案14】:

              管道是Pandas 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
              

              注意:Pandas 版本保留了 Python 的引用语义。这就是length_times_width 不需要返回值的原因;它修改了x

              【讨论】:

              • 不幸的是,这仅适用于数据帧,因此我无法将其指定为正确答案。但在这里值得一提的是,我想到的主要用例是将其应用于数据帧。
              【解决方案15】:

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

              与 Sylvain Leroux 提到的解决方案相比,主要优点是您不需要为您有兴趣使用的函数创建中缀对象——只需标记您打算使用转换的代码区域。其次,由于转换是在编译时而不是运行时应用的,所以转换后的代码在运行时不会受到任何开销——所有的工作都是在第一次从源代码生成字节码时完成的。

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

              示例代码:

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

              最后是完成艰苦工作的模块。我将其称为 fpipe 用于功能管道,因为它可以模拟 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)
              

              【讨论】:

              • 听起来不错,但我认为它只适用于 Python 2.7(而不是 Python 3.4)。
              • 我创建了一个较小的库,没有依赖项,它与@fpipe 装饰器做同样的事情,但重新定义右移 (>>) 而不是或 (|):pypi.org/project/pipeop
              • 被否决,因为需要使用多个装饰器的 3rd 方库对于一个相当简单的问题来说是一个非常复杂的解决方案。另外,它是一个只有 python 2 的解决方案。很确定香草 python 解决方案也会更快。
              【解决方案16】:

              python 语言是否支持类似的东西?

              “更实用的管道语法” 这真的是更“实用”的语法吗?我会说它为 R 添加了一个“中缀”语法。

              话虽如此,Python's grammar 不直接支持标准运算符之外的中缀表示法。


              如果你真的需要这样的东西,你应该以that code from Tomer Filiba为起点来实现你自己的中缀符号:

              Tomer Filiba (http://tomerfiliba.com/blog/Infix-Operators/) 的代码示例和 cmets:

              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
              

              【讨论】:

                猜你喜欢
                • 2014-11-15
                • 1970-01-01
                • 1970-01-01
                • 2017-10-02
                • 1970-01-01
                • 2020-09-05
                • 2018-07-23
                相关资源
                最近更新 更多