11

所以我在 Python 中玩弄柯里化函数,我注意到的一件事是 functools.partial 返回一个部分对象而不是一个实际函数。让我烦恼的一件事是,如果我做了以下事情:

five = partial(len, 'hello')
five('something')

然后我们得到

TypeError: len() takes exactly 1 argument (2 given)

但我想要发生的是

TypeError: five() takes no arguments (1 given)

有没有一种干净的方法可以让它像这样工作?我写了一个解决方法,但它对我的口味来说太老套了(对于带有可变参数的函数还不起作用):

def mypartial(f, *args):
  argcount = f.func_code.co_argcount - len(args)
  params = ''.join('a' + str(i) + ',' for i in xrange(argcount))
  code = '''
def func(f, args):
  def %s(%s):
    return f(*(args+(%s)))
  return %s
  ''' % (f.func_name, params, params, f.func_name)

  exec code in locals()
  return func(f, args)

编辑:我认为如果我添加更多上下文可能会有所帮助。我正在编写一个装饰器,它会自动对一个函数进行如下处理:

@curry
def add(a, b, c):
  return a + b + c

f = add(1, 2) # f is a function
assert f(5) == 8

我想隐藏 f 是从部分创建的事实(也许是个坏主意:P)。上面的 TypeError 消息给出的消息是可以揭示某物是否为局部的一个示例。我想改变它。

这需要可推广,因此 EnricoGiampieri 和 mgilson 的建议仅适用于该特定情况。

4

2 回答 2

4

你绝对不想用exec.

你可以在纯 Python 中找到食谱partial,比如这个——其中许多都被错误地标记为curry食谱,所以也要寻找它。无论如何,这些将向您展示在没有exec.

或者你可以直接包装partial……</p>

然而,无论你做什么,包装器都无法知道它定义了一个名为“five”的函数;这只是您存储函数的变量的名称。因此,如果您想要自定义名称,则必须将其传递给函数:

five = my_partial('five', len, 'hello')

那时,您必须想知道为什么这比仅仅定义一个新函数更好。

但是,无论如何,我认为这不是您真正想要的。您的最终目标是定义一个@curry装饰器,它创建装饰函数的柯里化版本,与装饰函数具有相同的名称(和文档字符串、参数列表等)。替换中间体名称的整个想法partial是红鲱鱼。functools.wraps在你的函数中正确使用,你curry如何定义柯里化函数并不重要,它会保留原始的名称。

在某些情况下,functools.wraps不起作用。事实上,这可能是其中一种情况——例如,您需要修改 arg 列表,因此curry(len)可以采用 0 或 1 个参数而不是 1 个参数,对吧?请参阅update_wrapper和(非常简单的)源代码,了解基础知识是如何工作的,并从那里构建wrapsupdate_wrapper

扩展前面的内容:要对函数进行柯里化,您几乎必须返回一些接受(*args)or(*args, **kw)并显式解析 args 的内容,并可能显式地引发TypeError和其他适当的异常。为什么?好吧,如果foo需要 3 个参数,curry(foo)则需要 0、1、2 或 3 个参数,如果给定 0-2 个参数,它会返回一个函数,该函数需要 0 到 n-1 个参数。

您可能想要的原因**kw是它允许调用者按名称指定参数 - 尽管当您完成累积参数时检查会变得更加复杂,并且可以说这与柯里化有关 - 它可能更好首先将命名参数与 绑定partial,然后curry是结果并以柯里化样式传递所有剩余的参数......</p>

如果foo有默认值或关键字参数,它会变得更加复杂,但即使没有这些问题,你已经需要处理这个问题。

例如,假设您实现curry为一个类,该类将函数和所有已经柯里化的参数作为实例成员。然后你会有这样的东西:

def __call__(self, *args):
    if len(args) + len(self.curried_args) > self.fn.func_code.co_argcount:
        raise TypeError('%s() takes exactly %d arguments (%d given)' %
                        (self.fn.func_name, self.fn.func_code.co_argcount,
                         len(args) + len(self.curried_args)))
    self.curried_args += args
    if len(self.curried_args) == self.fn.func_code.co_argcount:
        return self.fn(*self.curried_args)
    else:
        return self

这非常简单,但它显示了如何处理基础知识。

于 2012-11-20T23:05:57.340 回答
0

我的猜测是部分函数只是延迟了函数的执行,不要从中创建一个全新的函数。

我的猜测是直接在适当的位置定义一个新函数更容易:

def five(): return len('hello')

这是一个非常简单的行,不会使您的代码混乱并且非常清晰,因此我不会费心编写函数来替换它,特别是如果您在大量情况下不需要这种情况

于 2012-11-20T22:57:57.640 回答