不要在Python中编写 lambda 表达式了

在不讨论 lambda 表达式的情况下, 我很难深入地讲授 Python 类. 我经常遇到有关它们的问题. 学生们往往会在 StackOverflow 或者他们同事的代码中(实际上, 这个也可能来自StackOverflow)碰到他们.

我对 lambda 有很多的疑问, 我很犹豫是否要推荐学生接受 Python lambda 表达式. 多年来我一直都很厌恶 lambda 表达式, 自从几年前我开始频繁教授 Python 后, 我对它的厌恶与日俱增.

我将会说明我对 lambda 表达式的看法, 以及为何我倾向于建议学生们避免使用它.

Python 中的 lambda 表达式: 它们是什么?

lambda 表达式是 Python 中创建匿名函数的一个特殊语法. 我称 lambda 语法本身为 lambda 表达式, 而它返回的函数我称之为 lambda 函数.

Python 的 lambda 表达式允许在一行代码中创建一个函数并传递(通常传递到另外一个函数).

lambda 表达式允许我们使用此代码:

并将其转换为以下代码:

lambda 表达式仅仅是创建函数的一个特殊方法. 它们只包含一条语句, 并自动返回这条语句的结果.

lambda 表达式本身的局限性实际上是其吸引力的一部分. 当经验丰富的 Python 程序员看到一个lambda 表达式时, 他们知道他们正在使用一个仅在一个地方有效的函数, 并且只做一件事情.

如果你曾经在 JavaScript 中使用过匿名函数, 那么Python 中的 lambda 表达式与之相同, 除了具有更多限制以及与传统函数完全不同的语法.

通常使用的地方

你通常会在调用接受函数作为参数的函数(或类)时, 使用 lambda 表达式.

Python 内置的 sorted 函数接受一个函数作为它的 key 参数. 这个 key 函数用于在决定条目排序顺序时计算比较键的值.

所以 sorted 可以作为一个经常使用 lambda 表达式范例:

上述代码返回了对给定颜色以不区分大小写方式排序的结果.

sorted 函数并不是 lambda 表达式的唯一用法, 但却是最普遍的一个.

lambda 的利弊

围绕 lambda 表达式和 def 定义的函数之间的一系列对比, 我发表一下看法. 这两类工具都可以提供函数, 但它们都有各自的限制, 使用了不同的语法.

lambda 表达式与 def 的主要不同点:

可以立刻传递(无需变量)

在内部只能包含一行代码

自动返回结果

既没有文档字符串, 也没有名称

使用了不同且不常见的语法

事实上, lambda 表达式能够被传递是它们最大的优势. 自动返回结果很简洁, 但在我看来并不是很大的优势. "单行代码"的限制总体上不好不坏. 而 lambda 函数没有文档字符串和名称令人遗憾, 而它们的一些不常见的语法可能会对新的Pythonista造成困扰.

总的来说, 我觉得 lambda 表达式的缺点略微超过了它的优点, 但我对它们最大的怨念是它们往往被滥用或者过度使用.

lambda 被滥用和过度使用

当我在陌生代码中看到 lambda 表达式时, 我立刻会新生疑虑. 当我在自然环境下遇到 lambda 表达式时, 我经常发现去掉它们之后能提高代码的稳定性.

有时候 lambda 表达式会被滥用, 意味着它们的使用方式通常不理想. 另外有时候 lambda 表达式仅仅被过度使用, 意味着它们可以被接受, 但我个人更愿意看到以其他不同方式编写的代码.

让我们来看一下 lambda 表达式被滥用和过度使用的几种方式.

滥用: 命名 lambda 表达式

官方 Python 风格指南 PEP8 建议永远不要编写这样的代码:

上述语句创建了一个匿名函数并赋值到一个变量. 上面的代码忽视了用 lambda 的原因: lambda 函数可以被直接传递而无需先赋值给一个变量.

如果你想创建一个一行代码的函数并存储到变量中, 你应该使用def:

PEP8 推荐这种方式, 因为命名函数是一个常见并容易理解的东西. 同时给函数一个合适的名称也是很有好处的, 可以让调试简单一些. 而与 def 定义的函数不同, lambda 函数从来都没有一个名称(名称都是):

如果你想创建一个函数并存储到变量中, 请使用 def 来定义. 这正是它的用途. 无论你的函数是一行代码还是在另外一个函数中定义, 都可以, def 正适合这些应用场景.

滥用: 调用不必要的函数

我经常看到用 lambda 表达式封装一个已经很适合当前问题的函数.

例如这段代码:

写这段代码的人很可能了解过, 知道 lambda 表达式是用来创建一个可传递函数的. 但他们却忽略了一个更大一些的理念: Python 中所有的函数(不止 lambda 函数)都是可传递的.

既然 abs(返回一个数字的绝对值) 是一个函数并且所有函数都是可传递的, 实际上我们可以将上述代码编写为:

这个例子可能会让人感到有些假, 但以这种方式来使用 lambda 表达式并不十分罕见. 这是我看到的另外一个例子:

因为我们接受与我们传给 min 完全相同的参数, 所以完全没有必要调用额外的函数. 我们可以直接将 min 函数传递给 key:

如果你已经有了另一个符合你要求的函数, 则不需要一个 lambda 函数.

过度使用: 简单, 但不常用的函数

lambda 表达式通常用于创建一个在元组中返回一系列值的函数.

这里的 key 所传的函数让我们可以根据长度以及大小写标准化的名称来对颜色进行排序.

下面的代码与上面的功能相同, 但我认为更有可读性:

代码看上去有点啰嗦, 但我觉得 key 函数的名称可以让排序的依据更加清晰. 我们不是只依据长度排序, 也不是只依据颜色排序: 我们同时使用了两者.

如果一个函数很重要, 那么它应当有一个名称. 你可以争论说, lambda 表达式中使用的大多数函数都不重要, 不值得给一个名称, 但命名函数通常没什么缺点, 而且我发现它通常会使我的代码整体上可读性更好.

给函数命名通常会让代码更有可读性, 同样的, 使用元组拆包来命名变量而不是使用随机索引查找的方式通常会让代码更有可读性.

过度使用: 多行代码有帮助的时候

有时候 lambda 表达式"只有一行"这方面的特性会导致我们用复杂的方式来编写代码. 例如下面的例子:

在这里我们对索引查找做了硬编码以按照颜色来对点进行排序. 如果我们使用一个命名函数, 我们可以用元组拆包来让代码更有可读性:

比起使用硬编码索引查找,元组拆包可以提升可读性. 使用 lambda 表达式通常意味着牺牲掉一些 Python 语言的特性, 尤其是需要多行代码的时候(比如额外的赋值语句).

过度使用: lambda 与 map 和 filter

Python 的 map 和 filter 函数经常与 lambda 表达式搭配在一起使用. 当在 StackOverflow 上提问"什么是 lambda 表达式"的问题时, 经常会看到以下例子中的代码:

我发现这些例子有点令人困惑, 因为我几乎从未在我的代码中使用 map 和 filter.

Python 的 map 和 filter 函数用来循环并创建一个新的可迭代对象, 循环期间对每个元素做一些细微修改或者根据匹配的条件过滤到只剩一些元素. 我们完全可以只使用列表推导和生成器表达式来完成这两项任务:

就个人而言, 我更愿意看到上面的生成器表达式用多行代码来写(可以参见我关于推导的文章), 但我发现即使是这些单行的生成器表达式也比调用 map 和 filter 更具有可读性.

mapping 和 filtering 的一般操作是很有用的, 但我们实际上并不需要 map 和 filter 函数本身. 生成器表达式是一种特殊的语法, 仅适用于 mapping 和 filtering 任务. 所以我的建议是使用生成器表达式来替代 map 和 filter 函数.

滥用: 有时你甚至不需要去传递一个函数

那需要传递并执行单个操作函数的情况该怎么办?

热衷于函数式编程的新 Pythonistas 有时会写如下的代码:

上述代码对 numbers 列表中的所有数字做了加法. 但还有一个更好的方式来做这个操作:

Python 内置的 sum 函数就是专门做这个任务的.

sum 函数以及其他的一些专门的 Python 工具很容易被忽视. 但我建议你在需要时寻找更专业的工具, 因为它们通常会让代码更有可读性.

与其传递一个函数到其他函数中, **不如观察一下是否有更专业的方式来解决你的问题. **

过度使用: 使用 lambda 进行非常简单的操作

我们不说加法了, 再来说一下乘法吧:

上面的 lambda 表达式是很有必要的, 因为不允许我们传递 * 运算符, 就算它像一个函数一样. 如果有一个等价于 * 的函数, 我们就可以将它传递给 reduce 函数.

Python 的标准库实际上有一个完整的模块来解决这个问题:

Python 的运算符模块让 Python 的各种运算符像函数一样易用. 如果你正在练习函数式编程, Python 的 operator 模块就是你的好助手.

除了提供与 Python 许多运算符相对应的函数以外, operator 模块还提供了一系列常用的更高级的函数来访问条目和属性, 以及调用方法.

itemgetter 用来访问列表/序列的索引或字典/映射的键值:

attrgetter 用来访问对象的属性:

methodcaller 用来调用对象的方法:

我通常发现使用 operator 模块中的函数会使代码看上去比使用等效的 lambda 表达式更加清晰易懂.

过度使用: 当给高阶函数增加困惑时

一个接收其他函数作为参数的函数被称为高阶函数. 高阶函数通常就是我们经常向其传递 lambda 函数的那一类函数.

在练习函数式编程时高阶函数是很常用的. 然而函数式编程并不是应用 Python 思想的唯一方式: Python 是一种多范式语言, 因此我们可以混合并匹配编码规则, 让我们的代码更有可读性.

对比这个:

和这个:

第二段代码长一些, 但是没有函数式编程背景的人通常会觉得它更容易理解.

对很多 Python 程序员来说, reduce/lambda 的组合可能都会有些晦涩难懂.

通常, 将一个函数传递给另一个函数会使代码更加复杂, 这不利于代码的可读性.

你应该使用 lambda 表达式吗?

基于以下原因, 我觉得 lambda 表达式的应用是有问题的:

对很多 Python 程序员来说, lambda 表达式是一种古怪而又陌生的语法

lambda 函数本身缺少名称和文档, 意味着了解它们功能的唯一方式就是读代码

lambda 表达式只能包含一条语句, 因此某些提高可读性的语言功能, 如元组拆包, 不能与它们一起使用

lambda 函数通常可以被替换为标准库中已存在的函数或 Python 内置的函数

比起一个命名良好的函数, lambda 表达式缺乏即刻可读性. 尽管 def 语句通常更容易理解, 但 Python 还有很多可用于替换 lambda 表达式的功能, 包括特殊语法(推导), 内置函数(sum)和标准库函数(在 operator 模块中)

只有当你的情况完全满足这四个标准时, 我才会说你可以使用 lambda 表达式:

你所要做的操作是不重要的: 函数不值得一个名称

使用 lambda 表达式比你所能想到的函数名称让代码更容易理解

你很确定还没有一个函数能满足你的需求

你团队的每个人都了解 lambda 表达式, 并且都同意使用它们

如果上述四条中的任何一条都不符合你的情况, 我建议用 def 来写一个新的函数, (如果可能)接受一个在 Python 中已经存在且能满足你需求的函数.

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181110A1QKEG00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券