【问题标题】:Function composition, tuples and unpacking函数组合、元组和拆包
【发布时间】:2017-11-17 11:22:43
【问题描述】:

(声明:不是 Python 孩子,所以请温柔)

我正在尝试使用以下方法编写函数:

def compose(*functions):
  return functools.reduce(lambda acc, f: lambda x: acc(f(x)), functions, lambda x: x)

标量 函数按预期工作。我想使用返回元组的函数和其他接受多个参数的函数,例如。

def dummy(name):
  return (name, len(name), name.upper())

def transform(name, size, upper):
  return (upper, -size, name)

# What I want to achieve using composition, 
# ie. f = compose(transform, dummy)
transform(*dummy('Australia'))
=> ('AUSTRALIA', -9, 'Australia')

由于dummy 返回一个元组,而transform 接受三个参数,我需要解压缩该值。

如何使用上面的compose 函数实现这一点?如果我这样尝试,我会得到:

f = compose(transform, dummy)
f('Australia')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in <lambda>
  File "<stdin>", line 2, in <lambda>
TypeError: transform() takes exactly 3 arguments (1 given)

有没有办法改变compose,让它在需要的地方解压?

【问题讨论】:

    标签: python functional-programming function-composition


    【解决方案1】:

    这个适用于您的示例,但它不会处理任何任意函数 - 它只适用于位置参数,并且(当然)任何函数的签名都必须与前一个的返回值匹配(写/申请顺序)一个。

    def compose(*functions):
        return functools.reduce(
            lambda f, g: lambda *args: f(*g(*args)), 
            functions, 
            lambda *args: args
            )
    

    请注意,在这里使用reduce,虽然在函数式编程中肯定是惯用的,但相当不符合pythonic。 “显而易见的”pythonic 实现将使用迭代:

    def itercompose(*functions):
        def composed(*args):
            for func in reversed(functions):
                args = func(*args)
            return args    
        return composed
    

    编辑:

    你问“有没有办法让 compose 函数在这两种情况下都可以工作” - “两种情况”在这里意味着函数是否返回可迭代的(你称之为“标量函数”的概念,在 Python 中没有意义)。

    使用基于迭代的实现,您可以只测试返回值是否可迭代并将其包装在一个元组中,即:

    import collections
    
    def itercompose(*functions):
        def composed(*args):            
            for func in reversed(functions):
                if not isinstance(args, collections.Iterable):
                    args = (args,)
                args = func(*args)
            return args    
        return composed
    

    但这并不能保证按预期工作 - 实际上,对于大多数用例,这甚至可以保证不能按预期工作。 Python中有很多内置的可迭代类型(甚至更多用户定义的类型),仅仅知道一个对象是可迭代的并不能说明它的语义。

    例如 dictstr 是可迭代的,但在这种情况下显然应该被视为“标量”。 list 也是可迭代的,在这种情况下它应该如何解释实际上只是不确定的,而不知道它包含什么以及组合顺序中的“下一个”函数期望什么——在某些情况下,你会想把它当作一个单个参数,在其他情况下为参数列表。

    IOW 只有 compose() 函数的调用者才能真正知道应该如何考虑每个函数的结果 - 实际上,您甚至可能会遇到希望 tuple 被下一个函数视为“标量”值的情况.所以长话短说:不,Python 中没有万能的通用解决方案。我能想到的最好的方法需要将结果检查和组合函数的手动包装相结合,以便“组合”函数正确解释结果,但此时手动组合函数将更简单,更健壮。

    FWIW 请记住,Python 首先是一种动态类型的面向对象语言,因此虽然它确实对函数式编程习惯用法提供了不错的支持,但它显然不是真正的函数式编程的最佳工具。

    【讨论】:

    • 您介意详细说明一下它适用于您的示例并且不会只处理任意函数吗?你还有什么功能组合?从数学的角度来看,函数组合就是:映射的组合,传统上用 (g o f)(x) := g(f(x)) 表示。因此,g 的签名必须是f 的codomain 中的任何元素。这在像 Haskell 这样的类型化语言中是显而易见的。
    • 我的意思是它不能使用关键字 args 并且它要求每个函数的签名必须与前一个函数的返回值兼容,仅此而已。我想我实际上是在陈述一些非常明显的事情;)
    • 当然,我错过了 positionalkeywords 参数的对比。但是,您的命题不适用于标量函数,例如。 compose(operator.abs, operator.neg) TypeError: * 后面的 abs() 参数必须是序列,而不是 int。有没有办法让compose 函数在这两种情况下都有效? (这确实是我最初问题的意义)。
    • cf 我编辑的答案。 TL;DR :不,在 Python 中没有可靠的方法来实现它,无论你做什么,最终都会遇到太多的复杂情况和未处理/错误处理的极端情况。
    【解决方案2】:

    您可以考虑在您的 compose 链中插入一个“函数”(实际上是一个类构造函数),以表示先验/内部函数结果的解包。然后,您将调整您的作曲家函数以检查该类以确定是否应解压缩先前的结果。 (实际上你最终会做相反的事情:元组包装 all 函数结果 except 那些信号被解包的 - 然后让作曲家解包 everything。)它增加了开销,它根本不是 Pythonic,它是用简洁的 lambda 风格编写的,但它确实实现了当作曲家应该解压缩结果时能够在函数链中正确发出信号的目标。考虑以下通用代码,然后您可以将其适应您的特定组合链:

    from functools import reduce
    from operator import add
    
    class upk:  #class constructor signals composer to unpack prior result
      def __init__(s,r):  s.r = r  #hold function's return for wrapper function
    
    idt = lambda x: x  #identity
    wrp = lambda x: x.r if isinstance(x, upk) else (x,)  #wrap all but unpackables
    com = lambda *fs: (  #unpackable compose, unpacking whenever upk is encountered
      reduce(lambda a,f: lambda *x: a(*wrp(f(*x))), fs, idt) )
    
    foo = com(add, upk, divmod)  #upk signals divmod's results should be unpacked
    print(foo(6,4))
    

    这规避了之前的答案/cmets 所提出的问题,即要求您的作曲家猜测应该解压缩哪些类型的可迭代对象。当然,代价是您必须在需要解包时将upk 显式插入到可调用链中。从这个意义上说,它绝不是“自动”的,但它仍然是一种相当简单/简洁的方式,可以在许多极端情况下避免意外的包装/展开,同时实现预期结果。

    【讨论】:

      【解决方案3】:

      Bruno 提供的答案中的 compose 函数确实可以用于具有多个参数的函数,但不幸的是,它不再适用于标量函数。

      利用 Python 将元组“解包”到位置参数中这一事实,这就是我解决它的方法:

      import functools
      
      def compose(*functions):
        def pack(x): return x if type(x) is tuple else (x,)
      
        return functools.reduce(
          lambda acc, f: lambda *y: f(*pack(acc(*pack(y)))), reversed(functions), lambda *x: x)
      

      现在可以按预期工作,例如。

      #########################
      # scalar-valued functions
      #########################
      
      def a(x): return x + 1
      def b(x): return -x
      
      # explicit
      > a(b(b(a(15))))
      # => 17
      
      # compose
      > compose(a, b, b, a)(15)
      => 17
      
      
      ########################
      # tuple-valued functions
      ########################
      
      def dummy(x):
        return (x.upper(), len(x), x)
      def trans(a, b, c):
        return (b, c, a)
      
      # explicit
      > trans(*dummy('Australia'))
      # => ('AUSTRALIA', 9, 'Australia')
      
      # compose
      > compose(trans, dummy)('Australia')
      # => ('AUSTRALIA', 9, 'Australia')
      

      这也适用于多个参数:

      def add(x, y): return x + y
      
      # explicit
      > b(a(add(5, 3)))
      => -9
      
      # compose
      > compose(b, a, add)(5, 3)
      => -9
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-07-08
        • 1970-01-01
        • 2020-09-02
        • 1970-01-01
        相关资源
        最近更新 更多