为什么functools.partial没有返回真正的函数(以及如何创建一个函数)?

时间:2012-11-20 22:47:15

标签: python function partial currying

所以我正在玩Python中的currying函数,我注意到的一件事就是functools.partial返回一个部分对象而不是一个实际的函数。令我恼火的事情之一是,如果我按照以下方式做了一些事情:

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

然后我们得到

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

但我想要发生的是

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

是否有一种干净的方式让它像这样工作?我写了一个解决方法,但它对我的品味太过苛刻了(对于使用varargs的函数还没有用):

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的建议仅适用于该特定情况。

2 个答案:

答案 0 :(得分:4)

您绝对不希望使用exec执行此操作。

你可以在纯Python中找到partial的食谱,例如this one - 其中许多被错误标记为curry食谱,所以也要查找它。无论如何,这些都会在没有exec的情况下向您显示正确的方法,您可以选择一个并对其进行修改以达到您想要的效果。

或者你可以包裹partial ...

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

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

在那一点上,你不得不想知道为什么这比定义一个新函数更好。

但是,我认为这不是你真正想要的。你的最终目标是定义一个@curry装饰器,它创建一个装饰函数的curried版本,与装饰函数具有相同的名称(和docstring,arg list等)。替换中间人partial的名称的整个想法是一个红色的鲱鱼;在curry函数中正确使用functools.wraps,无论您如何定义curried函数,它都会保留原始名称。

在某些情况下,functools.wraps不起作用。事实上,这可能是其中一个时间 - 您需要修改arg列表,例如,curry(len)可以采用0或1参数而不需要1个参数,对吧?请参阅update_wrapper以及wrapsupdate_wrapper的{​​非常简单的} source code,了解基础知识的工作原理,并从那里开始构建。

扩展前一个:为了理解一个函数,你几乎必须返回一些需要(*args)(*args, **kw)的东西并明确地解析args,并可能引发TypeError和其他适当的明确的例外。为什么?好吧,如果foo需要3个参数,curry(foo)需要0,1,2或3个参数,如果给定0-2参数,则返回一个从0到n-1参数的函数。

你可能想要**kw的原因是它允许调用者按名称指定params - 虽然然后检查你在累积参数时会变得更复杂,并且可以说这是一个奇怪的事情与currying一起 - 最好先用partial绑定命名参数,然后curry结果并以咖喱风格传递所有剩余参数...

如果foo具有默认值或关键字args,则会变得更复杂,但即使没有这些问题,您也需要处理此问题。

例如,假设您将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

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

答案 1 :(得分:0)

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

我的猜测是,更容易直接定义新功能:

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

这是一个非常简单的行,不会使你的代码混乱并且非常清楚,所以我不打算写一个函数来替换它,特别是如果你在很多情况下不需要这种情况< / p>