为什么使用lambda
通常来说,lambda起到了一种函数速写的作用,允许在使用的代码内嵌入一个函数的定义。他们完全是可选的(你总是能够使用def来替代它们),但是你仅需要嵌入小段可执行代码的情况下它们会带来一个更简洁的代码结构。
例如,我们在稍后会看到回调处理器,它常常在一个注册调用(registration call)的参数列表中编写成单行的lambda表达式,而不是使用在文件其他地方的一个def来定义,之后引用那个变量名。
lambda通常用来编写跳转表(jump table),也就是行为的列表或字典,能够按照需要执行相应的动作。如下段代码所示。
1 2 3 4 5 6 7 8 | L = [ lambda x: x * * 2 , lambda x: x * * 3 , lambda x: x * * 4 ] for f in L: print (f( 2 )) # prints 4, 8, 16 print (L[ 0 ]( 3 )) # prints 9 |
当需要把小段的可执行代码编写进def语句从语法上不能编写进的地方时,lambda表达式作为def的一种速写来说是最为有用的。 例如,这种代码片段,可以通过在列表常量中嵌入lambda表达式创建一个含有三个函数的列表。一个def是不会再列表常量中工作的,因为它是一个语句,而不是一个表达式。 对等的def代码可能需要在想要使用的环境之外有临时性的函数名称和函数定义。
1 2 3 4 5 6 7 8 9 10 | def f1(x): return x * * 2 def f2(x): return x * * 3 def f3(x): return x * * 4 L = [f1, f2, f3] for f in L: print (f( 2 )) # prints 4, 8, 16 print (L[ 0 ]( 3 )) # prints 9 |
实际上,我们可以使用Python中的字典或者其他的数据结构来构建更多种类的行为表,从而做同样的事情。 下面是以交互提示模式给出的另一个例子:
1 2 3 4 5 | >>> key = 'got' >>> { 'already' : ( lambda : 2 + 2 ), ... 'got' : ( lambda : 2 * 4 ), ... 'one' : ( lambda : 2 * * 6 )}[key]() 8 |
这里,当Python常见这个字典的时候,每个嵌套的lambda都生成并留下了一个在之后能够调用的函数。通过键索引来取回其中一个函数,而括号使去除的函数被调用。 与在之前向你展示的if语句的扩展用法相比,这样编写代码可以使字典成为更加通用的多路分支工具。
如果不是用lambda做这种工作,需要使用三个文件中其他地方出现过的def语句来替代,也就是在这些函数将会使用的那个字典外的某处需要定义这些函数。
1 2 3 4 5 6 7 8 9 | >>> def f1(): return 2 + 2 ... >>> def f2(): return 2 * 4 ... >>> def f3(): return 2 * * 6 ... >>> key = 'one' >>> { 'already' : f1, 'got' : f2, 'one' : f3}[key]() 64 |
同样,会实现相同的功能,但是def也许会出现在文件中的任意位置,即使它们之后很少的代码。类似刚才lambda的代码,提供了一种特别有用的可以在单个情况出现的函数:如果这里的三个函数不会在其他的地方使用到,那么将它们定义作为lambda嵌入在字典中就很合理了。不仅如此,def格式要求为这些小函数创建变量名,这些变量名也许会与这个文件中的其他变量名发生冲突(也可能不会,但总是可能的)。
如何(不要)让Python代码变得晦涩难懂
由于lambda的主体必须是个表达式(而不是一些语句),由此可见仅能将有限的逻辑封装到一个lambda中。如果你知道在做什么,那么你就能在Python中作为基于表达式等效的写法编写足够多的语句。
例如,如果你希望在lambda函数中进行print,直接编写sys.stdout.write(str(x) + "\n") 这个表达式,而不是使用print(x)这样的语句。 类似地,要在一个lambda中潜逃逻辑,可以使用if/else三元表达式,或者对等的但需要些技巧的and/or组合。正如我们前面所了解到的,如下语句:
1 2 3 4 | if a: b else : c |
能够由以下的概括等效的表达式来模拟:
1 2 | b if a else c ((a and b) or c) |
因为这样类似的表达式能够放在lambda中,所以它们能够在lambda函数中来实现选择逻辑。
1 2 3 4 5 | >>> lower = ( lambda x, y: x if x < y else y) >>> lower( 'bb' , 'aa' ) 'aa' >>> lower( 'aa' , 'bb' ) 'aa' |
此外,如果需要在lambda函数中执行循环,能够嵌入map调用或列表解析表达式这样的工具来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> import sys >>> showall = lambda x: list ( map (sys.stdout.write, x)) >>> t = showall([ 'spam\n' , 'toast\n' , 'eggs\n' ]) spam toast eggs >>> showall = lambda x: [sys.stdout.write(line) for line in x] # 列表解析 >>> t = showall([ 'spam\n' , 'toast\n' , 'eggs\n' ]) spam toast eggs |
这些技巧必须在万不得已的情况下才使用。一不小心,它们就会导致不可读(也成为晦涩难懂)的Python代码。 一般来说,简洁优于复杂,明确优于晦涩,而且一个完整的语句要比神秘的表达式要好。 这就是为什么lambda仅限于表达式。如果你有更负责的代码要编写,可使用def, lambda针对较小的一段内联代码。 从另一个方面来说,你也会发现湿度的使用这些技术是很有用处的。
嵌套lambda和作用域
lambda是嵌套函数作用域查找(LEGB原则中的E)的最大受益者。 例如,在下面的例子中,lambda出现在def中(很典型的情况),并且在商城函数调用的时候,嵌套的lambda能够获取到上层函数作用域中的变量名x的值。
1 2 3 4 5 6 7 8 | >>> def action(x): return ( lambda y: x + y) >>> act = action( 99 ) >>> act <function action.< locals >.< lambda > at 0x0000014EF59F4C80 > >>> act( 2 ) 101 |
在之前讲关于嵌套函数作用域的讨论没有标明的就是lambda也能够获取任意上层lambda中的变量名。 这种情况有些隐晦,但是想象一下,如果我们上一个例子中高端def换成一个lambda。
1 2 3 4 5 6 | >>> action = ( lambda x:( lambda y: x + y)) >>> act = action( 99 ) >>> act( 3 ) 102 >>> (( lambda x: ( lambda y: x + y))( 99 ))( 4 ) 103 |
这里嵌套的lambda结构让函数在调用时创建了一个函数。无论以上那种情况,嵌套的lambda代码都能够获取上层lambda函数中的变量x。这可以工作,但是这种代码让人相当费解。处于可读性的要求,通常来说,最好避免使用嵌套的lambda。
=================================================================================
下面是一个例子。您可以以正常的方式用def构造一个函数,就像这样:
1 | def square_root(x): return math.sqrt(x) |
或者你可以用lambda
1 | square_root = lambda x: math.sqrt(x) |
下面是lambda的其他的一些有趣的例子:
1 2 3 | sum = lambda x, y: x + y # def sum(x,y): return x + y out = lambda *x: sys.stdout.write(" ".join(map(str,x))) lambda event, name=button8.getLabel(): self.onButton(event, name) |
lambda的好处在哪里? 已经困扰我有很长一段时间的一个问题是:lambda的好处在哪里?为什么我们需要lambda?
答案是: 我们并不需要lambda,我们不用它一样可以做所有的事情。但是… 在特定的情况下,很是方便 – 它让编写代码更容易一些,而且编写的代码更整洁。
什么样的情况? 好,其中一个情况是,我们需要一个简单的一次性功能:将被只使用一次函数。
通常,写函数有两个目的:(a)以减少代码重复(b)模块化代码。
- 如果你的应用程序在不同的地方包含重复的代码块,那么你就可以把代码拷贝到一个函数,给函数名,然后 – 使用该函数名 – 在代码中的不同位置调用它。
- 如果你有一个代码块执行一个明确的操作 – 但真的是冗长、粗糙、破坏程序的可读性,那么你可以把那么长的粗糙的所有代码变成一个函数。
但是,假设你需要创建一个函数,将只被使用一次 – 只从应用程序中的一个地方调用。好吧,首先,你不需要给函数的名称。它可以是“匿名的”。而且你可以把它定义在你想使用它的地方。这就是lambda是非常有用的时候。
但是,但是,但是…你会说。
- 首先是,为什么你想要一个只调用一次函数?排除原因(a)。
- 一个lambda的函数体只能包含单个表达式。这意味着,lambda表达式必须很短。排除了原因(b)。
创造一个短的匿名函数可能的原因是什么?
那么,考虑一下代码片段,使用lambda来定义一个Tkinter的GUI界面按钮的行为。 (这个例子是来自Mike的教程。)
1 2 3 4 5 6 7 8 9 10 | frame = tk.Frame(parent) frame.pack() btn22 = tk.Button(frame, text="22", command=lambda: self.printNum(22)) btn22.pack(side=tk.LEFT) btn44 = tk.Button(frame, text="44", command=lambda: self.printNum(44)) btn44.pack(side=tk.LEFT) |
这里要记住的一点是,tk.Button需要一个函数对象作为参数传递给该函数的参数。该函数对象将是它(按钮)点击按钮时调用的函数。基本上,该函数指定了点击该按钮时,GUI会做什么。
因此,我们必须通过函数参数传递一个函数对象到一个按钮。并注意 – 因为不同的按钮做不同的事情 – 我们需要为每个按钮对象提供不同的函数对象。每个函数将只使用一次。 所以,尽管我们可以这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | def __init__(self, parent): """Constructor""" frame = tk.Frame(parent) frame.pack() btn22 = tk.Button(frame, text="22", command=self.buttonCmd22) btn22.pack(side=tk.LEFT) btn44 = tk.Button(frame, text="44", command=self.buttonCmd44) btn44.pack(side=tk.LEFT) def buttonCmd22(self): self.printNum(22) def buttonCmd44(self): self.printNum(44) |
这样写更容易(且更清楚)
1 2 3 4 5 6 7 8 9 10 11 12 | def __init__(self, parent): """Constructor""" frame = tk.Frame(parent) frame.pack() btn22 = tk.Button(frame, text="22", command=lambda: self.printNum(22)) btn22.pack(side=tk.LEFT) btn44 = tk.Button(frame, text="44", command=lambda: self.printNum(44)) btn44.pack(side=tk.LEFT) |
当一个GUI程序有这样的代码,该按钮对象需要“call back”到被提供给作为其命令的函数对象。 因此,我们可以说,lambda的最常见的用途之一是在GUI框架,如Tkinter和wxPython中写“回调(callback)”,。
这一切似乎很简单。所以… 为什么lambda如此难以理解? 我能想到四个原因:
首先Lambda难以理解,因为:一个lambda只能用一个表达式:什么是表达式?
很多人想知道这个问题的答案。如果你在Google上搜索了一下,你会看到很多的帖子,“在Python中,表达式和语句之间的区别是什么?”
一个很好的答案是,表达式返回(或计算结果为)值,而语句则没有。不幸的是,在Python中表达式也可以是一个语句,这种情况很容易造成糊涂。 – 赋值语句就像 A = B = 0,Python支持链式赋值。 (Python不是C)
很多情况下在当人们问这个问题时,他们真正想知道的是:什么样的情况下可以放入lambda,什么情况下不可以? 而对于这个问题,我觉得遵循一些简单的规则就足够了。
- 如果它不返回一个值,它不是一个表达式,不能放入一个lambda。
- 如果你能想象它在赋值语句中放在等号的右边,那它是一个表达式,可以放进一个lambda。
利用这些规则意味着:
- 赋值语句不能在lambda中使用。在Python中,赋值语句不返回任何东西,甚至没有None(null)。
- 如数学运算,字符串操作,列表解析等都是一个lambda。
- 函数调用是表达式。可以在lambda中放置函数调用,并将参数传递给该函数。这样就在一个新的匿名函数中封装了原函数调用(参数其他内容)。
- 在Python3,print成了一个函数,所以在Python3+,print(…)可以在lambda中使用。
- 即使函数是返回None,就像在Python3print函数,可以在一个lambda中使用。
- [条件表达式],它是在Python2.5中引入,是表达式(而不是仅仅是一个语法不同的if / else语句)。它们返回一个值,并且可以在一个lambda使用。
1 2 | lambda: a if some_condition() else b lambda x: ‘big’ if x > 100 else ‘small’ |
难以理解的第二个原因是:一个lambda只有一个表达式:为什么?为什么只有一个?为什么不能多表达式?为什么不能是语句?
对于一些开发人员来说,这个问题的意思是为什么Python的lambda语法如此怪异?对于其他人,尤其是那些有Lisp的背景的,这个问题是指为什么Python的lambda这么残废?为什么不像Lisp的lambda那么强大?
答案是很复杂,它涉及Python语法的“pythonicity”。lambda是一个相对较晚加入Python的。它加入的时候,Python语法已经固定下来了。在这种情况下,语法的lambda必须用“Pythonic”的方式硬塞进一个已经建立好的Python语法中。导致可以在lambda表达式上来完成一些事情有一定的局限性。
坦率地说,我仍然认为lambda语法看起来有点怪异。尽管那样,但是Guido解释了为什么lambda的语法是不会改变的。 Python不会成为Lisp。
难以理解的第三个原因是::lambda通常被描述为一种工具,用于创建函数,但lambda语句中不含有返回语句。
在某种意义上,return语句隐含在lambda中。lambda规范必须包含只有一个表达式,表达式必须返回一个值,由lambda创建一个匿名函数隐式地返回表达式的返回值。这非常有意义。
还是 – 我想缺乏一个明确的return语句使得很难理解lambda,或者至少很难迅速理解。
难以理解的第四个原因是在lambda教程中通常会用作为创建匿名函数来引入lambda,其实最常见的lambda用途是用于创建匿名过程。
在编程的上古时期,我们就将子程序区分为两种不同的形式:过程和函数。过程是用来做事情的,并没有返回任何东西。函数是用于计算和返回值。函数和过程之间的差异已经成为一些编程语言的一部分了。在Pascal,例如,程序和函数是不同的关键字。
在大多数现代语言中,语言的语法中不再区分过程和函数。 例如Python的函数,可以像过程,函数,或两者兼而有之。(不是完全理想的)结果是一个Python函数总是被称为“函数”,即使它是本质上充当过程。
虽然过程和函数之间的区别已经基本消失的语言结构中,当思考有关程序如何工作的时候我们仍然时常用它。例如,当我读一个程序的源代码,并看到一些函数F,我揣摩F是做什么的。我经常可以把它归类到一个过程或函数 – 我会对自己说“F的目的是做这个的”,或“F的目的是计算和返回等这个和这个的”。
所以现在我想我们可以明白为什么lambda的许多解释是难以理解。 First of all, the Python language itself masks the distinction between a function and a procedure. 首先,Python语言本身模糊了函数和过程的区别。
第二,大多数教程介绍把lambda作为创建匿名函数的工具来介绍,其主要目的是要计算并返回结果。在大多数教程看到(这个包含)的第一个例子展示了如何编写一个lambda来返回值,x的平方根。
但是,这不是lambda最常用的方式,不是当他们在Google上搜索“python lambda教程”的时候要找的。对于lambda最常见的用途是创建匿名的过程,在GUI回调中使用。在这些用例中,我们不关心什么lambda返回什么,我们关心它做了什么。
这就解释了为什么典型的Python程序员难以理解大多数的lambda说明。因为他尝试学习如何编写一些GUI框架的代码:Tkinter,wxPython。运行这些lambda,想理解他们。Google“python lambda教程”。他发现那些以例子开始的教程是完全不适合他。
所以,如果你是这样的程序员 – 本教程是给你写的。我希望它能帮助到你。对不起,我们在本教程的结尾看到了这点,而不是开头。我们希望有一天,有人会写一个lambda教程,而不是以这种方式开头
- lambda是一个用来构造匿名函数的工具
而以这样的句子开始:
- lambda是一个用来构造回调的工具
所以你需要有它。另一个lambda教程。