lambda" 与 "operator.attrgetter('xxx')" 作为排序关键字函数的比较
我在看一些代码,发现里面有很多排序的调用,使用了比较函数。感觉应该用关键字函数来做。
如果你要改写这段代码 seq.sort(lambda x, y: cmp(x.xxx, y.xxx))
,你觉得下面哪种写法更好:
seq.sort(key=operator.attrgetter('xxx'))
还是:
seq.sort(key=lambda a: a.xxx)
我也想听听大家对修改已经能正常工作的代码有什么看法。
3 个回答
正如之前的评论者所说,attrgetter
确实稍微快一点,但在很多情况下,这种速度差异几乎可以忽略不计(大约是微秒级别)。
关于可读性,我个人更喜欢 lambda
,因为这个写法大家在不同的场合都见过,所以别人看起来可能更容易理解。
还有一个需要注意的地方是,当你使用 lambda
时,如果属性名写错了,你的开发工具会提醒你,而使用 attrgetter
时就不会有这个提示。
总的来说,我倾向于选择那些不需要额外导入的写法,只要替代方案足够简单易懂。
对现有的、能正常工作的代码进行修改,这就是程序发展的过程;-)。首先,写一套好的测试,这些测试能在现有代码下给出已知的结果,并把这些结果保存下来(在测试中通常叫做“金标准文件”);然后进行修改,重新运行测试,确保(最好是自动化的方式)测试结果的变化仅仅是你想要的那些变化——没有任何不希望出现的意外结果。当然,你可以使用更复杂的质量保证策略,但这就是很多“集成测试”方法的核心思想。
至于写简单的 key=
函数的两种方式,设计的初衷是让 operator.attrgetter
更快,因为它更专业化,但至少在当前版本的Python中,速度上没有明显的差别。既然如此,对于这种特殊情况,我推荐使用 lambda
,因为它更简洁和通用(而且我通常不是特别喜欢用lambda哦!)。
在选择使用 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)