首页 > 解决方案 > python函数中的奇怪返回值

问题描述

def cons(a, b):
    def pair(f):
        return f(a, b)
    return pair

def car(f):
    def left(a, b):
        return a
    return f(left)

def cdr(f):
    def right(a, b):
        return b
    return f(right)

在 git 上找到了这个 python 代码。

只想知道f(a,b)cons 定义中的内容是什么,它是如何工作的?(我猜不是一个功能)

标签: pythonpython-3.x

解决方案


在您知道 , 和 意思之前,这对您没有cons任何car意义cdr

在 Lisp 中,列表存储为一种非常简单的链表形式。一个列表要么是nil(如None)空列表,要么是一个值和另一个列表的对。该cons函数接受一个值和一个列表,并通过配对返回另一个列表:

def cons(head, rest):
    return (head, rest)

和函数carcdr它们代表“地址|数据寄存器的内容”,因为这些是用于在 1950 年代特定计算机上实现它们的汇编语言指令,但这不是很有帮助)从 a 返回第一个或第二个值一对:

def car(lst):
    return lst[0]
def cdr(lst):
    return lst[1]

所以,你可以列一个清单:

lst = cons(1, cons(2, cons(3, None)))

…你可以从中得到第二个值:

print(car(cdr(lst))

…你甚至可以编写函数来获取第 n 个值:

def nth(lst, n):
    if n == 0:
        return car(lst)
    return nth(cdr(lst), n-1)

…或打印出整个列表:

def printlist(lst):
    if lst:
        print(car(lst), end=' ')
        printlist(cdr(lst))

如果你了解这些是如何工作的,下一步就是在你发现的那些奇怪的定义上尝试它们。

他们仍然做同样的事情。所以,问题是:如何?更大的问题是:有什么意义?

好吧,使用这些奇怪的功能没有实际意义;真正的重点是向您展示计算机科学中的所有内容都可以仅使用函数编写,而不需要像元组这样的内置数据结构(甚至整数;只是需要不同的技巧)。

关键是高阶函数:将函数作为值和/或返回其他函数的函数。你实际上一直在使用这些: map,sort和 a key, 装饰器partial……它们只有在它们非常简单时才会令人困惑:

def car(f):
    def left(a, b):
        return a
    return f(left)

这需要一个函数,并在一个返回其两个参数中的第一个的函数上调用它。

并且cdr是相似的。

很难看到你会如何使用其中任何一个,直到你看到cons

def cons(a, b):
    def pair(f):
        return f(a, b)
    return pair

这需要两件事并返回一个函数,该函数接受另一个函数并将其应用于这两件事。


那么,我们从中得到cons(3, None)什么?我们得到一个函数,它接受一个函数,并将其应用于参数3None

def pair3(f):
    return f(3, None)

如果我们打电话cons(2, cons(3, None))

def pair23(f):
    return f(2, pair3)

如果你调用car那个函数会发生什么?追踪它:

def left(a, b):
    return a
return pair23(left)

这样pair23(left)做:

return left(2, pair3)

而且left很简单:

return 2

所以,我们得到了 的第一个元素(2, cons(3, None))


如果你打电话cdr怎么办?

def right(a, b):
    return a
return pair23(right)

这样pair23(right)做:

return right(2, pair3)

......而且right非常简单,所以它只是返回pair3

你可以算出,如果我们打电话给car(cdr(pair23)),我们就会摆脱3困境。

现在您可以编写lst = cons(1, cons(2, cons(3, None)))、编写上面的递归nthprintlist函数,并跟踪它们是如何工作的lst


我在上面提到你甚至可以摆脱整数。你是怎样做的?阅读有关教堂数字的信息。您定义zerosuccessor功能。然后你可以定义oneassuccessor(zero)twoas successor(one)。您甚至可以递归地定义addis add(x, zero)but xis add(x, successor(y))successor(add(x, y))然后继续定义mul等。

您还需要一个特殊函数,可以用作nil.

无论如何,一旦你这样做了,使用上面的所有其他定义,你就可以做到lst = cons(zero(cons(one, cons(two, cons(three, nil)))),并且nth(lst, two)会给你回报one。(当然写作printlist会有点棘手……)


显然,这一切都将比仅使用元组和整数等慢得多。但从理论上讲,这很有趣。

考虑一下:我们可以编写一个 Python 的小方言,它只有三种语句——<code>def、return和表达式语句——以及只有三种表达式——文字、标识符和函数调用——它可以做所有正常的事情Python 可以。(事实上​​,你可以通过使用 Python 已经拥有的函数定义表达式来完全摆脱语句。)这种小语言使用起来会很痛苦,但是编写一个程序来推理程序会容易得多用那种微小的语言。而且我们甚至知道如何将使用元组、循环等的代码翻译成这种微小的子集语言的代码,这意味着我们可以编写一个程序来解释真正的 Python 代码。

事实上,通过更多技巧(curried 函数和/或静态函数类型,以及惰性求值),编译器/解释器可以即时进行这种推理并为我们优化代码。很容易以编程方式判断car(cdr(cons(2, cons(3, None))将返回 3 而无需实际评估大多数函数调用,因此我们可以跳过评估它们并替换3整个表达式。

当然,如果任何功能可能有副作用,这就会崩溃。你显然不能仅仅替代Noneprint(3)得到相同的结果。因此,您需要一些巧妙的技巧,其中 IO 由一些魔术对象处理,该对象评估函数以确定它应该读取和写入的内容,然后整个程序的其余部分,即用户编写的部分,变得纯粹并且可以优化随你怎么便。有了更多的抽象,我们甚至可以使 IO 变得不必太神奇就可以做到这一点。

然后你可以建立一个标准库,把我们放弃的所有东西都还给你,用定义和调用函数的方式编写,所以它实际上是可用的——但在幕后,它只是减少了纯函数调用,这对于要优化的计算机。然后你基本上已经编写了 Haskell。


推荐阅读