Python为什么不支持多行Lambda表达式?

490

听说在Python中添加多行lambda表达式是不可能的,因为它们在语法上会与Python中的其他语法结构发生冲突。今天在公交车上思考这个问题时,我意识到我无法想到任何一个Python结构与多行lambda表达式发生冲突。考虑到我对该语言相当熟悉,这让我感到惊讶。

现在,我确定Guido没有将多行lambda表达式包含在语言中的原因,但出于好奇:包含多行lambda表达式会导致模糊不清的情况吗?我所听到的是真的吗?还是Python不允许多行lambda表达式的原因是其他方面的原因?


46
因为 Python 是一种没有 { } 代码块的“懒惰语言”,所以为了保持一致的语法设计,不允许这样做。 - Andrew
30
此外:我很惊讶在回答中没有人提到这一点……在Python中,您可以使用反斜杠字符“\”来结束一行并继续到下一行。这个信息有点超越了整个问题的范围,所以…… - Andrew
1
https://softwareengineering.stackexchange.com/questions/99243/why-doesnt-python-allow-multi-line-lambdas - pcworld
3
"语法设计" - nicolas
2
除了def不能与它要使用的逻辑一起排成一行外:您必须将其放在其他地方,然后读者必须去寻找它。对于仅使用一次的代码而言,拥有一个def是Python语言的严重缺陷:这些仅应该用于代码重用。 - WestCoastProjects
显示剩余5条评论
23个回答

831

Python的创始人Guido van Rossum在一篇旧博客文章中回答了这个确切的问题。他承认这在理论上是可能的,但是任何提出的解决方案都不符合Python的编程规范:

"但对于我来说,这个谜题的任何提议解决方案的复杂性都是巨大的:它需要解析器(或者更准确地说,词法分析器)能够在缩进敏感和缩进不敏感模式之间来回切换,并保持以前模式和缩进级别的堆栈。从技术上讲,所有这些都可以解决(已经有了缩进级别的堆栈可以被泛化)。但这并不能消除我内心深处对其过度复杂的直觉感受,这就像一个精巧的鲁伯·戈尔德堡机器。"


137
为什么这不是最佳答案?原因并非技术问题,而是一个设计选择,正如发明者明确说明的那样。 - Dan Abramov
22
因为楼主很可能已经多年没有登录了,所以 @DanAbramov 还未收到回复。 - Prof. Falken
76
Guido的回答是我希望Python不依赖缩进来定义代码块的又一个原因。 - Mr. Lance E Sloan
33
我不确定我会称“直觉”为设计选择。 ;) - Elliot Cameron
17
我还没有完全开始使用Python,但我一直在考虑。其中有一件事情让我想吐,就是我必须定义一个真正的函数名称才能将其用作高阶函数。这个“非Pythonic”的东西让我不想写Python。 - Joel M
显示剩余13条评论

184

看以下内容:

map(multilambda x:
      y=x+1
      return y
   , [1,2,3])

这个 lambda 函数返回的是 (y, [1,2,3]) 吗?这样 map 函数只会得到一个参数,导致出错?还是它返回 y?或者是语法错误,因为换行后的逗号位置不正确?Python 怎么知道你想要什么?

在括号中,缩进对 Python 来说并不重要,所以不能明确地处理多行代码。

这只是一个简单的例子,可能还有其他情况。


136
如果你想从lambda中返回一个元组,他们可以强制使用括号。在我看来,这种限制应该一直存在以防止这种模棱两可的情况发生,但是这样也无妨。 - mpen
36
这是一个简单的歧义,必须通过添加额外的括号来解决,这在许多地方已经存在,例如被其他参数包围的生成器表达式,对整数字面量调用方法(尽管这不一定是必须的,因为函数名不能以数字开头),当然还有单行lambda表达式(它们可能是多行编写的长表达式)。 多行lambda与这些情况并没有太大的区别,因此没有理由基于这个原因将其排除掉。 这里 是真正的答案。 - nmclean
16
我很喜欢有无数的语言可以处理它,不必担心,但是似乎有一些深刻的原因,为什么它被认为非常困难,甚至不可能。 - nicolas
4
@nicolas 这就是 Python 的要点概括。 - WestCoastProjects
3
对于函数式编程,我毫不犹豫地选择Scala而非Python。 - lostsoul29
显示剩余3条评论

84

这通常非常丑陋(但有时替代方案更加丑陋),因此一种解决方法是创建一个括号表达式:

lambda: (
    doFoo('abc'),
    doBar(123),
    doBaz())

它不会接受任何任务,因此您需要提前准备数据。

我发现这在PySide封装器中非常有用,有时候您可能需要使用短的回调函数。编写额外的成员函数将更加丑陋。通常情况下,您不需要这样做。

示例:

pushButtonShowDialog.clicked.connect(
    lambda: (
    field1.clear(),
    spinBox1.setValue(0),
    diag.show())

3
我的老板在我们的PyQt应用程序中正需要这样的东西。太棒了! - TheGerm
1
谢谢这个,我也在寻找一种好的方法来将短小(但仍然多行)的lambda函数作为我们PySide UI的回调函数使用。 - Michael Leonard
现在我已经看到了这个,它立即建议使用lambda argsetattr(arg,'attr','value')来规避“无赋值...”的问题。然后有andor的短路求值...这是Javascript做的。就像常春藤一样扎根于你的内心,几乎希望自己能在圣诞节忘记这件事情。 - nigel222
1
@nigel222 为什么感到羞愧?Python语言在根本上是有缺陷的,但它仍然是许多数据科学领域使用的语言。因此,我们需要进行调整。找到处理副作用的方法(通常足以简单地打印/记录!)和赋值(通常足以使用中间变量!)应该由语言很好地处理。但是它们甚至没有得到支持(至少如果您遵循PEP指南)。 - WestCoastProjects
@nigel222 是的,赋值运算符现在可以近似多行lambda函数!请给 这个答案 点赞。此外,这是一个演示它使用的 示例 - Venryx
显示剩余3条评论

22

一些相关链接:

我曾经一段时间关注Reia的发展情况,它最初打算在Erlang的基础上采用Python的缩进语法和Ruby块,但是设计者最终放弃了缩进灵敏度。他写的这篇文章包括了他在缩进和多行块方面遇到的问题,并增加了他对Guido的设计问题/决策的欣赏:

http://www.unlimitednovelty.com/2009/03/indentation-sensitivity-post-mortem.html

此外,我看到了一个有趣的提议,建议在Python中采用Ruby风格的块,Guido发布了回复,没有否定它(不确定是否有任何后续的否定):

http://tav.espians.com/ruby-style-blocks-in-python.html


19

让我向您展示一个光荣而可怕的黑客技巧:

import types

def _obj():
  return lambda: None

def LET(bindings, body, env=None):
  '''Introduce local bindings.
  ex: LET(('a', 1,
           'b', 2),
          lambda o: [o.a, o.b])
  gives: [1, 2]

  Bindings down the chain can depend on
  the ones above them through a lambda.
  ex: LET(('a', 1,
           'b', lambda o: o.a + 1),
          lambda o: o.b)
  gives: 2
  '''
  if len(bindings) == 0:
    return body(env)

  env = env or _obj()
  k, v = bindings[:2]
  if isinstance(v, types.FunctionType):
    v = v(env)

  setattr(env, k, v)
  return LET(bindings[2:], body, env)

您现在可以这样使用LET表单:

map(lambda x: LET(('y', x + 1,
                   'z', x - 1),
                  lambda o: o.y * o.z),
    [1, 2, 3])

结果是:[0, 3, 8]


2
最初发布于https://gist.github.com/divs1210/d218d4b747b08751b2a232260321cdeb - divs1210
4
太棒了!我想下次写Python时会用到这个。我主要是Lisp和JS程序员,缺少多行lambda让我很困扰。这是一个获得它的方法。 - Alexis Dumas
虽然我喜欢Lisp,也喜欢Python,但我不喜欢在Python中写Lisp风格的代码。这是不符合Python风格的。 - Iiridayn
@liridayn 其他人发现这个链接很有用:https://gist.github.com/divs1210/d218d4b747b08751b2a232260321cdeb?permalink_comment_id=4356540#gistcomment-4356540 - undefined

18

我在一些项目中犯了这个肮脏的技巧,它有点简单:

    lambda args...:( expr1, expr2, expr3, ...,
            exprN, returnExpr)[-1]

我希望你能找到一种保持Python风格的方法,但如果你必须这样做,那么使用exec和操作全局变量的方式会比这种方法更加痛苦。


是的,快到了。与赋值表达式一起使用,它会更加有用。 - Severin Pappadeux
如果你定义了一个函数 def multiline(*expressions): return expressions[-1],那么你可以写成 lambda args...: multiline(expr1, ..., returnExpr),这样读起来会更加自然。 - user118967
很丑,但是能用。 - Zhuo YING

17

[编辑编辑]由于这个问题在12年后仍然活跃,所以我将继续传统,每隔4年左右修订一次我的答案。

首先,问题是关于如何处理Python中的多行Lambda函数。被接受的答案展示了一个简单的例子。我下面链接的高评分答案回答了“为什么它不是Python的一部分”的问题——这个答案对那些认为现有的“冲突”示例不足以使多行Lambda函数不可能在Python中实现的人可能更加满意。

在此答案的以前版本中,我曾经讨论过如何按原样将多行Lambda函数实现到Python中。但是,我已经将那部分删除,因为它是一堆不良做法。如果您愿意,可以在此答案的编辑历史记录中看到它。

然而,“为什么不呢?”的答案是“因为Rossum这样说”,仍然可能引起沮丧。因此,让我们看看是否可以通过用户balpha给出的反例来解决这个问题:

map(lambda x:
        y=x+1 # <-- this line defines the outmost indent level*
        for i in range(12):
            y+=12
        return y
   , [1,2,3])

#*By convention it is always one-indent past the 'l' in lambda

至于返回值,我们可以得出以下在Python中不允许的内容:

def f():
  return 3
, [1,2,3]

按照同样的逻辑,“[1,2,3]”不应该是返回值的一部分。相反,我们可以试试这种方法:

map(lambda x:
        y=x+1                    # part of lambda block
        for i in range(12):      # part of lambda block
            y+=12                # part of lambda block
        return y, [1,2,3])       # part of lambda block

这个有点棘手,但是由于lambda块有一个明确定义的开始(标记“lambda”),但没有明确的结束,我认为与lambda块的一部分在同一行上的任何东西也都是lambda块的一部分。

人们可能会想象一些功能,可以识别闭合括号,甚至基于包围元素所期望的标记数量的推断。总的来说,上述表达式似乎并不完全不可能解析,但可能有一点挑战。

为了简化事情,您可以分离所有不打算成为该块一部分的字符:

map(lambda x:
        y=x+1                    # part of lambda block
        for i in range(12):      # part of lambda block
            y+=12                # part of lambda block
        return y                 # part of lambda block
, [1,2,3]) # argument separator, second argument, and closing paren for map

回到我们之前的话题,但这次不会有歧义了,因为最后一行在lambda块的最低缩进深度之后。

单行lambda将是一个特殊情况(通过颜色后面没有立即换行符标识),它的行为与现在相同。

这并不是说它必须成为Python的一部分 - 但这是一个快速说明,可能需要对语言进行一些更改才能实现。

[编辑] 阅读这个答案。 它解释了为什么多行lambda不可行。

简而言之,这是不符合Python风格的。根据Guido van Rossum的博客文章:

我发现任何将缩进基础块嵌入表达式中间的解决方案都是不可接受的。由于我认为语句分组的替代语法(例如大括号或begin/end关键字)也同样不可接受,这几乎使得多行lambda成为一个无法解决的难题。


10
让我也加入我的一点建议,提供不同的解决方案。
一个简单的一行lambda函数与普通函数有什么区别?我只能想到缺少赋值、一些类似循环的结构(for,while),try-except子句...就这样?我们甚至有了三元操作符-太酷了!所以,让我们试着来解决每个问题。
赋值
这里有些人正确地指出我们应该看一下lisp的let形式,它允许本地绑定。实际上,所有非状态更改的赋值都可以通过let来执行。但是每个lisp程序员都知道,let形式绝对等同于调用lambda函数!这意味着
(let ([x_ x] [y_ y])
  (do-sth-with-x-&-y x_ y_))

是相同的意思

((lambda (x_ y_)
   (do-sth-with-x-&-y x_ y_)) x y)

所以lambda函数已经足够了!每当我们想要创建一个新的赋值时,我们只需要添加另一个lambda函数并调用它。考虑以下示例:
def f(x):
    y = f1(x)
    z = f2(x, y)
    return y,z

一个 lambda 版本长这样:
f = lambda x: (lambda y: (y, f2(x,y)))(f1(x))

如果你不喜欢数据在操作后被写入,你甚至可以创建let函数。而且你甚至可以将其柯里化(出于更多的括号的考虑 :)

let = curry(lambda args, f: f(*args))
f_lmb = lambda x: let((f1(x),), lambda y: (y, f2(x,y)))
# or:
f_lmb = lambda x: let((f1(x),))(lambda y: (y, f2(x,y)))

# even better alternative:
let = lambda *args: lambda f: f(*args)
f_lmb = lambda x: let(f1(x))(lambda y: (y, f2(x,y)))

到目前为止还不错。但是如果我们需要重新分配任务,即改变状态怎么办?嗯,我认为只要任务不涉及循环,我们就可以完全愉快地生活,不必改变状态。

循环

虽然没有循环的直接lambda替代方案,但我相信我们可以编写相当通用的函数来满足我们的需求。看一下这个斐波那契函数:

def fib(n):
    k = 0
    fib_k, fib_k_plus_1 = 0, 1
    while k < n:
        k += 1
        fib_k_plus_1, fib_k = fib_k_plus_1 + fib_k, fib_k_plus_1
    return fib_k

显然,使用 lambda 函数是不可能的。但是,写一个简单而实用的函数之后,我们就可以解决这个问题和类似的情况:

def loop(first_state, condition, state_changer):
    state = first_state
    while condition(*state):
        state = state_changer(*state)
    return state

fib_lmb = lambda n:\
            loop(
              (0,0,1),
              lambda k, fib_k, fib_k_plus_1:\
                k < n,
              lambda k, fib_k, fib_k_plus_1:\
                (k+1, fib_k_plus_1, fib_k_plus_1 + fib_k))[1]

当然,如果可能的话,应该始终考虑使用mapreduce和其他高阶函数。

尝试-异常处理及其他控制结构

对于这种问题,似乎一般的方法是利用惰性求值,用接受零参数的 lambda 表达式替换代码块:

def f(x):
    try:    return len(x)
    except: return 0
# the same as:
def try_except_f(try_clause, except_clause):
    try: return try_clause()
    except: return except_clause()
f = lambda x: try_except_f(lambda: len(x), lambda: 0)
# f(-1) -> 0
# f([1,2,3]) -> 3

当然,这并不是完全替代try-except语句的方案,但你总是可以把它变得更通用。顺便说一下,有了这种方法,您甚至可以使if像函数一样运行!

总结一下:所有提到的东西都感觉有点不自然和不太Python风格美丽。尽管如此- 它确实可以工作!而且没有任何eval和其他技巧,因此所有的智能提示都将起作用。我也不是声称您应该在每个地方都使用它。大多数情况下,最好定义一个普通函数。我只是想证明没有什么是不可能的。


太疯狂了!酷毙了! - plinyar
@aoeu256 我不是很明白,您能否提供一些例子或文档呢? - heinwol
这非常有帮助。虽然我永远不会编写真正的代码,但我使用许多将Python作为扩展语言并需要某些输入表达式的工具。这些技巧让我能够扩展我可以输入的表达式的边界。到目前为止,我的记录包括将300行过程性代码转换为单个函数表达式。 - pavon

9

Python3.8之后,还有一种本地绑定的方法。

lambda x: (
    y := x + 1,
    y ** 2
)[-1]

For循环

lambda x: (
    y := x ** 2,
    [y := y + x for _ in range(10)],
    y
)[-1]

条件语句

lambda x: (
    y := x ** 2,
    x > 5 and [y := y + x for _ in range(10)],
    y
)[-1]

或者
lambda x: (
    y := x ** 2,
    [y := y + x for _ in range(10)] if x > 5 else None,
    y
)[-1]

循环语句

import itertools as it
lambda x: (
    l := dict(y = x ** 2),
    cond := lambda: l['y'] < 100,
    body := lambda: l.update(y = l['y'] + x),
    *it.takewhile(lambda _: cond() and (body(), True)[-1], it.count()),
    l['y']
)[-1]

或者

import itertools as it
from types import SimpleNamespace as ns
lambda x: (
    l := ns(y = x ** 2),
    cond := lambda: l.y < 100,
    body := lambda: vars(l).update(y = l.y + x),
    *it.takewhile(lambda _: cond() and (body(), True)[-1], it.count()),
    l.y
)[-1]

或者

import itertools as it
lambda x: (
    y := x ** 2,
    *it.takewhile(lambda t: t[0],
    ((
    pred := y < 100,
    pred and (y := y + x))
    for _ in it.count())),
    y
)[-1]

6
让我试着解决@balpha的解析问题。我会在多行lambda周围使用括号。如果没有括号,lambda定义是贪婪的。因此,在中的lambda应该改为:
map(lambda x:
      y = x+1
      z = x-1
      y*z,
    [1,2,3]))

返回一个函数,该函数返回(y*z,[1,2,3])

但是

map((lambda x:
      y = x+1
      z = x-1
      y*z)
    ,[1,2,3]))

意思是

map(func, [1,2,3])

func是一个多行lambda函数,返回值为y*z。这样行得通吗?


1
我认为顶部的应该返回map(func, [1,2,3]),而底部的应该是一个错误,因为map函数没有足够的参数。此外,代码中有一些额外的括号。 - Samie Bencherif
将其放入运行Python2.7.13的Pycharm中,会出现语法错误。 - simbo1905
额外的括号 - Samie Bencherif
好的,我们现在有表达式赋值了,它可能有效,请查看我下面的答案。 - Severin Pappadeux

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接