【问题标题】:Function chaining in PythonPython中的函数链
【发布时间】:2016-12-26 13:47:14
【问题描述】:

Codewars.com我遇到了以下任务:

创建一个函数add,在连续调用时将数字相加。所以add(1) 应该返回1add(1)(2) 应该返回1+2,...

虽然我熟悉 Python 的基础知识,但我从未遇到过能够连续调用的函数,即可以调用为 f(x)(y)(z)... 的函数 f(x)。到目前为止,我什至不确定如何解释这个符号。

作为一名数学家,我怀疑f(x)(y) 是一个函数,它为每个x 分配一个函数g_{x},然后返回g_{x}(y)f(x)(y)(z)

如果这种解释是正确的,Python 将允许我动态创建对我来说非常有趣的函数。在过去的一个小时里,我在网上搜索了一个小时,但无法找到正确方向的线索。不过,由于我不知道这个编程概念是如何被调用的,所以这可能不会太令人惊讶。

你如何称呼这个概念,我在哪里可以阅读更多关于它的信息?

【问题讨论】:

  • 看起来你正在寻找柯里化函数
  • 提示:嵌套函数是动态创建的,可以访问其父函数的局部变量,并且能够作为(可调用的)对象返回。
  • @JonathonReinhart 这就是我思考问题的方式。但我并没有真正看到如何实现它。
  • 顺便说一句:Python肯定允许你动态创建函数。如果您有兴趣,可以阅读以下几个相关概念:WP: First-class functions | How do you make a higher order function in Python? | functools.partial() | WP: Closures
  • @LukasGraf 我会看看它。谢谢!

标签: python function python-3.x


【解决方案1】:

如果您愿意接受额外的() 以检索结果,您可以使用functools.partial

from functools import partial

def add(*args, result=0):
    return partial(add, result=sum(args)+result) if args else result

例如:

>>> add(1)
functools.partial(<function add at 0x7ffbcf3ff430>, result=1)
>>> add(1)(2)
functools.partial(<function add at 0x7ffbcf3ff430>, result=3)
>>> add(1)(2)()
3

这也允许一次指定多个数字:

>>> add(1, 2, 3)(4, 5)(6)()
21

如果您想将其限制为单个数字,您可以执行以下操作:

def add(x=None, *, result=0):
    return partial(add, result=x+result) if x is not None else result

如果您希望 add(x)(y)(z) 轻松返回结果并且可以进一步调用,那么子类化 int 是要走的路。

【讨论】:

    【解决方案2】:

    简单地说:

    class add(int):
       def __call__(self, n):
          return add(self + n)
    

    【讨论】:

    • 这无疑是最好的答案!
    【解决方案3】:

    我不知道这是否是 function 链接和 callable 链接一样多,但是,由于函数 are 是可调用的,我猜有没有造成伤害。无论哪种方式,我都可以想到两种方法:

    子类int并定义__call__

    第一种方法是使用自定义 int 子类,它定义了 __call__,它返回具有更新值的自身新实例:

    class CustomInt(int):
        def __call__(self, v):
            return CustomInt(self + v)
    

    现在可以定义函数add 以返回一个CustomInt 实例,该实例作为一个返回自身更新值的可调用对象,可以被连续调用:

    >>> def add(v):
    ...    return CustomInt(v)
    >>> add(1)
    1
    >>> add(1)(2)
    3
    >>> add(1)(2)(3)(44)  # and so on..
    50
    

    另外,作为int的子类,返回值保留ints的__repr____str__行为。 不过,对于更复杂的操作,您应该适当地定义其他 dunders

    正如@Caridorc 在评论中指出的那样,add 也可以简单地写成:

    add = CustomInt 
    

    将类重命名为 add 而不是 CustomInt 也同样有效。


    定义一个闭包,需要额外调用 yield 值:

    我能想到的唯一其他方法涉及一个嵌套函数,该函数需要一个额外的空参数调用才能返回结果。我使用 nonlocal 并选择将属性附加到函数对象以使其在 Python 之间可移植:

    def add(v):
        def _inner_adder(val=None):  
            """ 
            if val is None we return _inner_adder.v 
            else we increment and return ourselves
            """
            if val is None:    
                return _inner_adder.v
            _inner_adder.v += val
            return _inner_adder
        _inner_adder.v = v  # save value
        return _inner_adder 
    

    这会不断返回自身 (_inner_adder),如果提供了 val,则将其递增 (_inner_adder += val),如果没有,则按原样返回值。就像我提到的,它需要一个额外的() 调用才能返回增加的值:

    >>> add(1)(2)()
    3
    >>> add(1)(2)(3)()  # and so on..
    6
    

    【讨论】:

    • 在交互代码中add = CostumInt 应该也可以工作并且更简单。
    • 子类化内置函数的问题是(2*add(1)(2))(3) 失败并返回TypeError,因为int 不可调用。基本上,CustomIntany 上下文中使用时会转换为普通的int,调用时除外。要获得更强大的解决方案,您基本上必须重新实现所有 __*__ 方法,包括 __r*__ 版本...
    • @Caridorc 或者在定义它时根本不叫它CustomInt 而叫它add
    【解决方案4】:

    pythonic 的方法是使用动态参数:

    def add(*args):
        return sum(args)
    

    这不是您要寻找的答案,您可能知道这一点,但我想我还是会给出它,因为如果有人想知道这样做不是出于好奇而是为了工作。他们可能应该有“正确的事情”的答案。

    【讨论】:

    • 我删除了你的“P.S”字条,nichochar。我们都知道 Python 有多优雅 :-) 我认为它不属于答案的主体。
    • 我确实认为如果要走那条路,你本来可以完成add = sum
    【解决方案5】:

    你可以恨我,但这里是一条线:)

    add = lambda v: type("", (int,), {"__call__": lambda self, v: self.__class__(self + v)})(v)
    

    编辑:好的,这是如何工作的?该代码与@Jim 的答案相同,但一切都发生在一行中。

    1. type 可用于构造新类型:type(name, bases, dict) -&gt; a new type。对于name,我们提供空字符串,因为在这种情况下实际上不需要名称。对于bases(元组),我们提供(int,),这与继承int 相同。 dict 是类属性,我们在其中附加 __call__ lambda。
    2. self.__class__(self + v) 等同于 return CustomInt(self + v)
    3. 新类型在外部 lambda 中构造并返回。

    【讨论】:

    • 甚至更短:class add(int):__call__ = lambda self, v: add(self+v)
    • 类中的代码与普通代码完全相同,因此您可以通过赋值定义特殊方法。唯一的区别是类范围有点……奇特。
    【解决方案6】:

    如果你想定义一个函数被多次调用,首先你需要每次都返回一个可调用对象(例如一个函数),否则你必须通过定义一个__call__属性来创建你自己的对象,为了使其可调用。

    下一点是您需要保留所有参数,在这种情况下,这意味着您可能想要使用Coroutines 或递归函数。但请注意,协程比递归函数更优化/更灵活,特别是对于此类任务。

    这是一个使用协程的示例函数,它保留了自身的最新状态。请注意,它不能被多次调用,因为返回值是不可调用的 integer,但您可能会考虑将其转换为您的预期对象 ;-)。

    def add():
        current = yield
        while True:
            value = yield current
            current = value + current
    
    
    it = add()
    next(it)
    print(it.send(10))
    print(it.send(2))
    print(it.send(4))
    
    10
    12
    16
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多