功能组合,元组和拆包

时间:2017-11-17 11:22:43

标签: python functional-programming function-composition

(声明:不是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,以便在需要的地方解压缩?

3 个答案:

答案 0 :(得分:1)

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

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

请注意,在这里使用reduce虽然在函数式编程中肯定是惯用的,但却相当单一。 “明显的”pythonic实现将使用迭代:

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

编辑:

你问“有没有办法让一个能在两种情况下都有效的组合函数” - “两种情况”这里意味着函数返回一个可迭代的或者不是(你称之为“标量函数”,这个概念是在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也是可迭代的,在这种情况下如何解释它实际上是不可判断的,如果不确切知道它包含什么以及组合顺序中的“下一个”函数需要什么 - 在某些情况下你会想要对它进行处理作为单个参数,在其他情况下使用args列表。

IOW只有compose()函数的调用者才能真正告诉我们应该如何考虑每个函数结果 - 实际上你甚至可能需要将tuple视为“标量”值的情况通过下一个功能。所以简而言之:不,在Python中没有一个通用的通用解决方案。我能想到的最好需要结合检查和组合函数的手动包装,因此结果可以通过“组合”函数正确解释,但此时手动组合函数将更简单,更稳健。

FWIW记得Python是第一个并且主要是动态类型的面向对象语言,所以虽然它确实对函数式编程习惯有很好的支持,但它显然不是真正的函数式编程的最佳工具。

答案 1 :(得分:1)

您可能会考虑在撰写链中插入“函数”(实际上是类构造函数),以表示对先前/内部函数的结果进行了拆包。然后,您将调整composer函数以检查该类,以确定是否应该解压缩先前的结果。 (实际上,您最终做相反的事情:元组换行 all 函数结果 那些被信号提示要解压的结果-然后让作曲者解压一切 >。)这会增加开销,它根本不是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))

就像先前的答案/评论所指出的那样,这避免了要求作曲家猜测应该拆开哪些类型的可迭代对象的问题。当然,这样做的代价是,无论何时需要拆包,都必须将upk明确插入可调用链中。从这个意义上讲,它绝不是“自动的”,但它仍然是获得预期结果的相当简单/简洁的方法,同时在许多特殊情况下避免了意外的换行。

答案 2 :(得分:0)

Bruno提供的答案中的compose函数确实为具有多个参数的函数执行了工作,但不幸的是,它不再适用于标量函数。

使用Python`解包&#39;元组成位置参数,这就是我解决它的方法:

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