λ表达式及其组合
前言
我们再来看看第1课中定义的toTfunc()
函数。
def toTfunc(func):
def res(tuple):
return func(*tuple)
return res
def multiply(a, b):
return a * b
请注意,toTfunc()
主体内的这个res()
函数有点多余,我们希望定义更简洁。对于这个multiply()
函数来说也是如此,尽管它很简单,但它需要所有这些句法结构。
第1部分 - λ表达式
为了避免显式编写def
关键字,我们可以使用 匿名函数 语法。要以这种方式创建函数,我们使用lambda
关键字,通过逗号枚举所有参数,然后在冒号符号后写下作为函数体的表达式。例如,我们可以像这样定义乘法和加法函数。
mul = lambda a, b: a * b
add = lambda a, b: a + b
或者我们可以像这样避免在toTfunc
主体中创建冗余函数。
def toTfunc(func):
return (lambda tuple: func(*tuple))
我们甚至可能两次使用这样的定义。这样我们就创建了一个函数,给定参数f
,返回一个函数。
toTfunc = lambda f: (lambda t: f(*t))
此语法可用于通过显式指定一个或多个参数的值来减少函数采用的参数数量。
mul2 = lambda x: mul(x, 2)
add1 = lambda x: add(x, 1)
您还可以利用堆叠多个lambda
来创建一个函数,该函数分别接受每个参数。也就是说,每次提供一个参数并返回一个函数,其主体中的未绑定参数实际上更少。
sep_mul = lambda a: lambda b: a * b
print(sep_mul(2)(3))
>>> 6
mul2 = sep_mul(2)
print(mul2(3))
>>> 6
我还想提一下,许多人发现lambda
这个关键字太长了,它真的很快就会将屏幕弄得乱七八糟。可以在fn
模块中找到替代语法。比如下边这个示例:
from fn import _
mul = _ * _
mul2 = _ * 2
square = _ ** 2
print(mul(2, 3), square(4))
>>> 6 16
基本上每个下划线对应一个单独的参数,文档中说这种语法类似于Scala[1]。 我[2]避免使用它,因为下划线与为我指定函数的冗余参数[3]密切相关,但我想你们中的一些人可能会发现使用这种符号编写会更愉快。我还建议您查看 GitHub 上的 fn.py 文档,它是一个旨在使 Python 中的函数式编程更加简单的模块。
现在,回顾一下nextSquare()
函数。
from math import sqrt
def inc(x):
return x + 1
def fnextSquare(x):
return pow(inc(sqrt(x)), 2)
由于我们需要的函数很简单,我们可能会以函数方式编写它。无需实际定义所有函数,而是使用匿名函数[4]。
lnextSquare = lambda x: (lambda y: y ** 2)((lambda z: z + 1)(sqrt(x)))
print(lnextSquare(25))
>>> 36.0
第2部分 - 组合
但我们也可以选择另一种方式来做到这一点。请注意,在这种情况下,在函数体中写入参数有点多余。我的意思是,我们仍根据其结果如何取决于其参数来定义函数。但是当我们定义这个nextSquare()
函数时,它不是我们想要表达的依赖关系,我们想说要得到下一个平方,你先取平方根,然后增加,最后得到其平方。为了专注于函数的这个含义,我们可以使用 组合 。函数 toolz
模块中已经有一个了。
from toolz import compose
square = lambda x: x ** 2
inc = lambda x: x + 1
sqrt = lambda x: x ** 0.5
cnextSquare = compose(square, inc, sqrt)
print(cnextSquare(25))
>>> 36.0
使用这个compose
函数,我们可以以一种非常直接的方式定义nextSquare
。这里唯一需要注意的是,组合函数的顺序很重要,组合链中的最后一个函数将首先应用,依此类推。顺便说一下,这种定义函数而不将其参数作为参数写入函数体内的方法称为 无点样式 [5],它通常用于函数式编程。
第3部分 - 邱琦计数法
我想在这节课中展示的最后一件事,是用一个例子来说明匿名函数的机制与λ表达式的起源之间的联系。您可能听说过 图灵机 ——由阿兰·图灵[6]开发的一种抽象计算模型,该模型基于评估器在通过对其执行一些操作来处理某些数据存储[7]时改变其状态的想法。阿隆佐·邱琦[8]开发了另一种称为 λ演算 的模型。与图灵模型不同,在λ演算中,数据和操作之间没有严格的区别。也就是说,我们可以将数据表示为函数,并且可以像操作数据一样操作函数,因此数据和操作是同构的。该模型是被认为是函数式编程语言[9]的核心。其中一些概念使程序员能够做更多的事情,以至于许多语言都采用了其中的一部分。您已经知道在 Python 中将函数传递给其他函数以及从其他函数返回函数的能力,所以这个『操作函数就像操作数据一样』是很清楚的。那么,什么是『将数据表示为函数』呢?我想向你展示这个叫做 邱琦计数 的系统,它使用λ表达式来表示数字。
让我们将数字
zero = lambda f: lambda x: x
one = lambda f: lambda x: f(x)
two = lambda f: lambda x: f(f(x))
three = lambda f: lambda x: f(f(f(x)))
因此,零是一个函数,在给定任何函数
succ = lambda n: lambda f: lambda x: f(n(f)(x))
为了检查它是否有效,我们可以用一个具体的
f = lambda x: x + 1
x = 0
zero(f)(x)
>>> 0
one(f)(x)
>>> 1
two(f)(x)
>>> 2
three(f)(x)
>>> 3
现在,让我们找出将
succ(three)(f)(x)
>>> 4
它真的起作用了!我想再次指出,这些函数是『数字函数』,而不是应用某些特定
f = compose(chr, inc, ord)
x = 'A'
zero(f)(x)
>>> 'A'
one(f)(x)
>>> 'B'
two(f)(x)
>>> 'C'
three(f)(x)
>>> 'D'
succ(three)(f)(x)
>>> 'E'
现在,让我们想一些方法来对这些数字执行加法和乘法。加法应该是一个函数,给定数字
plus = lambda m: lambda n: lambda f: lambda x: m(f)(n(f)(x))
print(plus(one)(two)(f)(x))
>>> 3
再一次,乘法函数应该取数字
times = lambda m: lambda n: lambda f: m(n(f))
print(times(two)(three)(f)(x))
print(times(three)(succ(three))(f)(x))
>>> 6
..| 12
在 Python 布尔值之外的、敌对的外星世界中,布尔值不是数字。因此,将布尔值表示为数字并不是一个令人满意的解决方案,因为我们已经实现了后者。这是布尔值作为函数的实现:
true = lambda x: lambda y: x
false = lambda x: lambda y: y
怎么理解?如果我们通过定义
print(true(1)(0))
>>> 1
很好,但这一切都与能够对数据进行操作有关。我们如何实现常见的逻辑功能?
让我们创建一个『逻辑与』函数。其实它没那么复杂:采用两个布尔值
另一个要制作的功能是『逻辑非』。好吧,如果该函数表示的值取决于它返回哪一个参数,则翻转调用提供的参数的顺序会改变该函数的布尔值。
und = lambda b1: lambda b2: b1(b2)(b1)
print('tt: {}, tf: {}, ft: {}, ff: {}'.format(
und(true)(true)(1)(0),
und(true)(false)(1)(0),
und(false)(true)(1)(0),
und(false)(false)(1)(0)
))
nut = lambda b: lambda x: lambda y: b(y)(x)
print(nut(true)(1)(0))
>>> tt: 1, tf: 0, ft: 0, ff: 0
..| 0
你可以用它做很多其他的事情,但这就是这一课的内容。如果你对它很好奇、认为它充满挑战,你可以尝试学习更多关于λ演算和组合逻辑在编程中的用法。这些是更高级、理论性更强的主题,但您可能会发现它们值得探索。
总结
在这一课中,我们介绍了λ表达式,并说明了如何在 Python 中进行定义,顺便还提了一下能让 Python 如 Scala 般顺滑的fn
库;我们还讲解了函数之间的组合,提到了 Python 中的toolz
库;我们最后讲解了邱琦计数法,比较了图灵机和λ演算的思维差异,还通过实际代码实现了基本的计数和数学运算,更以另外的方式定义了布尔值并实现逻辑运算。
嘿,我不就好这口嘛~ ↩︎
此处指原作者,但我(本人)觉得这种写法确实很香。 ↩︎
也就是所谓的通配符,但我(本人)有时也会将它当哑变量(定义了但从不使用)占位符。 ↩︎
lambda x: (sqrt(x) + 1) ** 2
的写法不好嘛?所有运算符都是返回新值的,根本不可能更改形参好吧? ↩︎或者叫做 默认编程 。 ↩︎
英文原名为 Alan Turing 。 ↩︎
在大部分教材提及这件事时,示意图都会将这个『存储』画成一条『无限磁带』的样子。 ↩︎
英文原名为 Alonzo Church ,有些文章会写作『邱奇』。Who cares? ↩︎
比如 Lisp 与 Haskell 。前面提到的 Scala 虽然也有函数式特性存在,但它本身是多编程范式语言,玩不了邱琦计数这样的纯函数式『魔法』。 ↩︎
也就是『自增函数』。 ↩︎