0%

在Python里如何将min()和max() 与嵌套列表一起使用 (译)

本文用于练习英文阅读,如有侵权,联系删除

原文链接Dan Bader的 How to use Python’s min() and max() with nested lists

以下为译文
让我们谈谈在包含另一个列表的列表中使用Python的min()和max()函数。有时这被成为嵌套列表或列表索引。

根据内部特定属性查找列表的最大或最小元素是一种常见情况,但对与Python新手来说可能是一个挑战。

下面是一个具体实例,假设这样一个列表,包含权重部分。

1
nested_list = [['cherry', 7], ['apple', 100], ['anaconda', 1360]]

我们希望Python 根据储存在索引1上的权重选择最小和最大元素。我们预期min和max返回下面元素:

  • min(nested_list) should be ['cherry', 7]
  • max(nested_list) should be ['anaconda', 1360]

但是仅在nested_list 上调用 min 和 max 我们不能得到预期结果。

我们得到的排序似乎基于存储在索引0处的商品名称:

1
2
3
4
5
>>> min(nested_list)
['anaconda', 1360] # 非预期!

>>> max(nested_list)
['cherry', 7] # 非预期!

好吧,为什么选错了元素呢?

让我们停下来思考一下Python的max函数工作原理。算法看起来像这样

1
2
3
4
5
6
7
8
9
10
11
12
def my_max(sequence):
"""Return the maximum element of a sequence"""
if not sequence:
raise ValueError('empty sequence')

maximum = sequence[0]

for item in sequence:
if item > maximum:
maximum = item

return maximum

选择最大值的条件下会发现这个有趣的行为: if item > amximum:如果序列的类型只是int 或 float,它能很好的工作,因为比较他们很简单(它符合我们的直觉,例如 3 > 2)。

但是,如果序列包含其他序列,则情况会变得更加复杂。让我们看一下Python 文档去学习下 比较序列和其他类型

序列对象可以与相同类型的其他对象比较。它们使用 字典顺序 进行比较:首先比较两个序列的第一个元素,如果不同,那么这就决定了比较操作的结果。如果它们相同,就再比较每个序列的第二个元素,以此类推,直到有一个序列被耗尽。如果要比较的两个元素本身就是相同类型的序列,那么就递归进行字典顺序比较。

当max需要比较两个序列并寻找最大的元素,默认可能不是我们想要的。

现在我们了解了为什么会得到意外结果,我们就可以去思考怎样修复代码。

我们如何改变比较行为

我们需要告诉max函数去比较不同的行为。

在上个例子,Python的max函数查看每一个列表的第一个元素(字符串 cherry, apple, or anaconda)并与当前最大元素进行比较,这就是为什么我们仅调用max(nested_list) 它将cherry返回作为最大元素的原因。

怎样告诉max 去比较每个列表的第二个元素呢?

假设我们有一个名叫my_max_by_weight的my_max更新版,它使用第二个元素进行比较每个内嵌列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
def my_max_by_weight(sequence):
if not sequence:
raise ValueError('empty sequence')

maximum = sequence[0]

for item in sequence:
# Compare elements by their weight stored
# in their second element.
if item[1] > maximum[1]:
maximum = item

return maximum

这很成功,我们可以看到my_max_by_weight 选择了我们预期的最大元素:

1
2
>>> my_max_by_weight(nested_list)
['anaconda', 1360]

现在假设我们需要在更多类型的list里寻找最大元素。

也许索引(或键)不总是第二项。可能有时是第三,或第四项,或着其他方式查找。

如果我们在my_max里可以复用大部分代码岂不更好?它的某些部分始终相同,例如 检查是否是空序列。

怎样让我们的max()更灵活?

因为Python允许我们把函数当作数据用,所以我们可以吧比较键的代码提取到自己的函数里。我们称之关键函数。我们编写各种不同key 函数,并根据需要传递给my_max。

这让我们的完整的灵活性。不仅可以比较特定列表索引位置,像索引1活着索引2,我们还可以告诉函数比较其他内容,例如,项目名称长度。

让我们看一下具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def identity(x):
return x

def my_max(sequence, key_func=None):
"""
Return the maximum element of a sequence.
key_func is an optional one-argument ordering function.
"""
if not sequence:
raise ValueError('empty sequence')

if not key_func:
key_func = identity

maximum = sequence[0]

for item in sequence:
# Ask the key func which property to compare
if key_func(item) > key_func(maximum):
maximum = item

return maximum

在代码示例中,您可以看到在默认情况下,如何调用称为identity的键函数,该函数只使用未修改的项进行比较。
将identity 作为key 函数,我们预期my_max 和之前max 行为一样。

1
2
3
4
nested_list = [['cherry', 7], ['apple', 100], ['anaconda', 1360]]

>>> my_max(nested_list)
['cherry', 7]

并且,我们仍然可以确定和之前(不正确的)结果一样,这很好的表明我们没有完全搞清实现机制。

现在最酷的部分——_我们通过编写key_func 来覆盖比较行为,该key_func 返回第二个子元素而不是元素本身:

1
2
3
4
5
def weight(x):
return x[1]

>>> my_max(nested_list, key_func=weight)
['anaconda', 1360]

瞧,这就是我们预期的最大元素!

只是为了演示此重构带来的灵活性,这个key_func 根据每一项的名字的长度,返回最大元素:

1
2
3
4
5
def name_length(x):
return len(x[0])

>>> my_max(nested_list, key_func=name_length)
['anaconda', 1360]

这有简写么?

除了用def显式定义key_func 函数外,也可以使用 Python lambda 表达式 去定义匿名函数。这大大缩短了代码(并不会创建命名函数)

1
2
my_max(nested_list, key_func=lambda x: x[1])
>>> ['anaconda', 1360]

为了让命名更简短(尽管表达力有点差) 假设我们缩短 key_func 参数 key ,并且让我们得出了和Python 的max函数兼容的代码。
这以为这我们不在需要自己重新实现 Python的max函数来查找正确的最大元素:

1
2
3
# This is pure, vanilla Python:
>>> max(nested_list, key=lambda x: x[1])
['anaconda', 1360]

Python 内置函数min 同样可以正常工作:

1
2
>>> min(nested_list, key=lambda x: x[1])
['cherry', 7]

它甚至适用于Python的sorted功能,"关键函数"概念对在Python 开发中遇到的很多问题都有价值:

1
2
>>> sorted(nested_list, key=lambda x: x[1])
[['cherry', 7], ['apple', 100], ['anaconda', 1360]]

欢迎关注我的其它发布渠道