【问题标题】:"lambda" vs. "operator.attrgetter('xxx')" as a sort key function"lambda" vs. "operator.attrgetter('xxx')" 作为排序键函数
【发布时间】:2010-04-24 15:56:37
【问题描述】:

我正在查看一些使用比较函数进行大量排序调用的代码,它似乎应该使用关键函数。

如果您要更改seq.sort(lambda x,y: cmp(x.xxx, y.xxx)),最好:

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

或:

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

我也会对 cme​​ts 感兴趣,因为它可以更改现有的有效代码。

【问题讨论】:

  • 根据meta 的讨论,该问题已被取消删除。 OP,您愿意接受 python3.7+ 的最新答案吗?
  • 无论如何 - 我对 cme​​ts 很感兴趣,即“一个明显优于另一个”,但没有说明哪个是哪个。
  • 根据经验,非 lambda 回调(尤其是以 C 速度运行的运算符函数)已证明在性能方面优于 lambda。

标签: python lambda


【解决方案1】:

当纯粹在 attrgetter('attributename')lambda o: o.attributename 之间选择作为排序键时,使用 attrgetter() 是两者中更快的选项。

记住key函数只对列表中的每个元素应用一次,在排序之前,所以我们可以在计时赛中直接使用它们来比较两者:

>>> 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 μs。这是因为调用 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)

【讨论】:

  • 我想了解native function such as produced by attrgetter() 是什么以及为什么它更快。我调查了attrgetter source code。因此,例如在排序时,会调用 attrgetter 的 __call__(),而后者又会调用 Python 函数 self._call()。我不知道 lambda 是如何在内部构造的,但不会发生完全相同的事情(调用__call__(),然后调用实际的 Python 函数)?我错过了什么?
  • @onepiece:operator 模块实现仅在无法导入 C 扩展的情况下存在。 real 源代码位于Modules/_operator.cimported into operator.py,替换了Python 版本。本地函数是指在您的 Python 安装随附的用 C 编写的扩展模块中实现的对象。
  • 我明白了。以下是我对直接比较的理解: 当attrgetter 被调用时: 1. 调用attrgetter_call()(C 级别) - Python 如何知道在调用attrgetter 实例时使用它? 1a。查找并返回属性(C 级)。对于 lambda: 1. 调用 __call__()(Python 级别) 2. 调用 lambda 的实际方法(Python 级别) 2a。查找并返回属性(Python 级别)。
  • 我不确定我的 lambda 理解是否正确 - lambda/函数的源代码在哪里?但是假设是这样,由于__call__(),lambda 有一个额外的 Python 调用,其余步骤从 Python 级别开始并继续到 C,而对于 attrgetter,这些步骤从 C 级别开始。
  • @onepiece:C 实现中没有_call。 Python 实现只是使用了一个间接函数调用(_call 设置为两个选项之一,在__init__)来简化区分多个属性(返回一个元组)和单个属性(返回一个值)的逻辑案例。
【解决方案2】:

“对现有的有效代码进行更改”是程序的发展方式;-)。使用现有代码编写一组好的测试,给出已知结果,保存这些结果(在测试上下文中通常称为“黄金文件”);然后进行更改,重新运行测试,并验证(理想情况下以自动化方式)测试结果的唯一更改是那些明确打算存在的更改 - 没有不受欢迎或意外的一面效果。当然,可以使用更复杂的质量保证策略,但这是许多“集成测试”方法的要点。

至于编写简单key= 函数的两种方法,设计意图是通过更专业化使operator.attrgetter 更快,但至少在当前版本的Python 中没有可测量的速度差异。既然如此,对于这种特殊情况,我会推荐lambda,只是因为它更简洁通用(而且我通常不是 lambda 爱好者,请注意!-)。

【讨论】:

  • 我同意亚历克斯的回答(那是他不是 lambda 爱好者):-) 但也支持 lambda 的概念。在上面的示例中,它可能会快一点,因为您不必查找operatoroperator.attrgetter()...您已经拥有实际的函数对象!然而,正如 alex 已经说过的那样,它几乎没有引起注意,但lambda 解决方案确实通过更易于阅读而赢得了 Python Zen 积分。
  • @wescpy attrgetterlambda 必须做相同数量的工作来查找属性,并且在您开始排序时都创建一次。但是 lambda 每次调用都需要一个新的 Python Stack 框架(相对昂贵),而 attrgetter 实例不需要,所以 attrgetter 更快。我发现内联 lambda 并不比 attrgetter 更容易阅读,所以对我来说,attrgetter 胜出。
【解决方案3】:

正如之前的评论者所说,attrgetter 稍微快一些,但在很多情况下,差异是微不足道的(~微秒)。

关于可读性,我个人更喜欢lambda,因为它是人们以前在不同情况下见过的结构,因此其他人可能更容易阅读和理解。

另一个需要注意的是,与使用 attrgetter 不同,在使用 lambda 时,您的 IDE 应该能够在 attr 名称上显示拼写错误。

一般来说,如果替代方案易于编写和阅读,我倾向于选择不需要额外导入的结构。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-01-24
    • 1970-01-01
    • 1970-01-01
    • 2014-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多