lambda" 与 "operator.attrgetter('xxx')" 作为排序关键字函数的比较

44 投票
3 回答
11131 浏览
提问于 2025-04-15 21:58

我在看一些代码,发现里面有很多排序的调用,使用了比较函数。感觉应该用关键字函数来做。

如果你要改写这段代码 seq.sort(lambda x, y: cmp(x.xxx, y.xxx)),你觉得下面哪种写法更好:

seq.sort(key=operator.attrgetter('xxx'))

还是:

seq.sort(key=lambda a: a.xxx)

我也想听听大家对修改已经能正常工作的代码有什么看法。

3 个回答

6

正如之前的评论者所说,attrgetter 确实稍微快一点,但在很多情况下,这种速度差异几乎可以忽略不计(大约是微秒级别)。

关于可读性,我个人更喜欢 lambda,因为这个写法大家在不同的场合都见过,所以别人看起来可能更容易理解。

还有一个需要注意的地方是,当你使用 lambda 时,如果属性名写错了,你的开发工具会提醒你,而使用 attrgetter 时就不会有这个提示。

总的来说,我倾向于选择那些不需要额外导入的写法,只要替代方案足够简单易懂。

24

对现有的、能正常工作的代码进行修改,这就是程序发展的过程;-)。首先,写一套好的测试,这些测试能在现有代码下给出已知的结果,并把这些结果保存下来(在测试中通常叫做“金标准文件”);然后进行修改,重新运行测试,确保(最好是自动化的方式)测试结果的变化仅仅是你想要的那些变化——没有任何不希望出现的意外结果。当然,你可以使用更复杂的质量保证策略,但这就是很多“集成测试”方法的核心思想。

至于写简单的 key= 函数的两种方式,设计的初衷是让 operator.attrgetter 更快,因为它更专业化,但至少在当前版本的Python中,速度上没有明显的差别。既然如此,对于这种特殊情况,我推荐使用 lambda,因为它更简洁和通用(而且我通常不是特别喜欢用lambda哦!)。

30

在选择使用 attrgetter('attributename')lambda o: o.attributename 作为排序的关键字时,使用 attrgetter() 是两者中更的选择。

要记住,关键函数只会在排序前对列表中的每个元素应用一次,所以我们可以直接用它们来进行时间测试,比较一下它们的速度:

>>> from timeit import Timer
>>> from random import randint
>>> from dataclasses import dataclass, field
>>> @dataclass
... class Foo:
...     bar: int = field(default_factory=lambda: randint(1, 10**6))
...
>>> testdata = [Foo() for _ in range(1000)]
>>> def test_function(objects, key):
...     [key(o) for o in objects]
...
>>> stmt = 't(testdata, key)'
>>> setup = 'from __main__ import test_function as t, testdata; '
>>> tests = {
...     'lambda': setup + 'key=lambda o: o.bar',
...     'attrgetter': setup + 'from operator import attrgetter; key=attrgetter("bar")'
... }
>>> for name, tsetup in tests.items():
...     count, total = Timer(stmt, tsetup).autorange()
...     print(f"{name:>10}: {total / count * 10 ** 6:7.3f} microseconds ({count} repetitions)")
...
    lambda: 130.495 microseconds (2000 repetitions)
attrgetter:  92.850 microseconds (5000 repetitions)

所以,调用 attrgetter('bar') 1000 次大约比 lambda 快 40 微秒。这是因为调用一个 Python 函数会有一定的开销,而调用像 attrgetter() 这样的本地函数开销要小一些。

这种速度优势也意味着排序会更快:

>>> def test_function(objects, key):
...     sorted(objects, key=key)
...
>>> for name, tsetup in tests.items():
...     count, total = Timer(stmt, tsetup).autorange()
...     print(f"{name:>10}: {total / count * 10 ** 6:7.3f} microseconds ({count} repetitions)")
...
    lambda: 218.715 microseconds (1000 repetitions)
attrgetter: 169.064 microseconds (2000 repetitions)

撰写回答