函数组合、元组和解包

4

(免责声明:我不是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个回答

1
这个可以在您的例子中工作,但它无法处理任何任意函数 - 它只能使用位置参数,并且(当然)任何函数的签名必须与前一个函数的返回值匹配(wrt /应用程序顺序)。
def compose(*functions):
    return functools.reduce(
        lambda f, g: lambda *args: f(*g(*args)), 
        functions, 
        lambda *args: args
        )

请注意,在此使用reduce虽然在函数式编程中很常见,但不太符合Python的惯用方式。更符合Python习惯的实现方式应该使用迭代:
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中有很多内置的可迭代类型(甚至更多的用户定义类型),仅仅知道一个对象是可迭代的并不能说明它的语义。例如,字典或字符串是可迭代的,但在这种情况下显然应该被视为“标量”。列表也是可迭代的,如果不知道列表包含的内容以及组合顺序中的“下一个”函数需要什么,那么如何解释它实际上是无法确定的——在某些情况下,您将希望将其视为单个参数,在其他情况下则将其视为参数列表。
只有compose()函数的调用者才能真正确定如何考虑每个函数的结果,实际上,你甚至可能有这样的情况,希望下一个函数将tuple视为“标量”值。所以,长话短说:在Python中没有一种通用的解决方案适用于所有情况。我能想到的最好的办法是需要组合结果检查和手动包装组合函数,以便“组合”函数正确地解释结果,但此时手动组合函数将更加简单和稳健。
值得一提的是,Python首先且主要是一种动态类型的面向对象语言,因此虽然它对函数式编程习惯有相当良好的支持,但显然不是实现真正函数式编程的最佳工具。

您介意详细说明一下“它将适用于您的示例,而不仅仅是处理任意函数”吗?否则,您如何进行函数组合?从数学角度来看,函数组合就是映射的组合,传统上用_(g o f)(x) := g(f(x))_表示。因此,_g_的签名必须是_f_的值域中的任何元素。在像Haskell这样的类型语言中,这是显而易见的。 - VH-NZZ
我的意思是它不能使用关键字参数,并且要求每个函数的签名必须与前一个函数的返回值兼容,仅此而已。我想我实际上陈述了一些非常明显的东西 ;) - bruno desthuilliers
当然,我错过了_positional_ vs. keywords args的部分。但是你的建议对于标量函数并不起作用,例如compose(operator.abs, operator.neg)会出现_TypeError: abs() argument after * must be a sequence, not int_。有没有办法让compose函数在这两种情况下都能正常工作?(这才是我最初问题的意义所在)。 - VH-NZZ
根据我的修改后的答案,总之,无论你做什么,都没有可靠的方法在Python中实现这个功能。你最终会遇到太多的复杂情况和未处理/错误处理的边角情况。 - bruno desthuilliers

1

在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

1
您可以考虑在组合器链中插入一个“函数”(实际上是一个类构造函数),以信号化前一个/内部函数的结果的拆包。然后,您将调整组合器函数以检查该类是否确定应解包先前的结果。 (实际上,您最终会做相反的事情:元组包装所有函数结果除了那些被标记为要解包的结果-然后让组合器解包一切。)它增加了开销,不太符合 Python 风格,用简洁的 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。从这个意义上说,它绝不是“自动”的,但在避免许多边缘情况下的意外包装/解包的同时,仍然是一种相当简单/简洁的实现预期结果的方式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接