"лямбда" против "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)
Мне также будет интересен комментарий о преимуществах внесения изменений в существующий код, который работает.
Ответы
Ответ 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 раз примерно на 40 мкс быстрее, чем lambda
. Это потому, что вызов функции 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)
Ответ 2
"Внесение изменений в существующий код, который работает" - это то, как развиваются программы;-). Напишите хорошую батарею тестов, которые дают известные результаты с существующим кодом, сохраните те результаты (которые обычно называются "золотыми файлами" в контексте тестирования); затем выполните изменения, повторите тесты и проверьте (в идеале автоматическим способом), что единственными изменениями результатов тестов являются те, которые специально предназначены, чтобы быть там - не было нежелательной или неожиданной стороны последствия. Конечно, можно использовать более сложные стратегии обеспечения качества, но это является основой многих подходов к интеграции.
Что касается двух способов написания простой функции key=
, целью дизайна было сделать operator.attrgetter
быстрее, будучи более специализированным, но по крайней мере в текущих версиях Python нет заметной разницы в скорости. В этом случае, для этой особой ситуации я бы рекомендовал lambda
, просто потому, что он более краткий и общий (и я обычно не лямбда-любовник, заметьте!).
Ответ 3
Как указывалось в предыдущих комментариях, attrgetter
немного быстрее, но для многих ситуаций разница незначительна (~ микросекунды).
Что касается читабельности, я лично предпочитаю lambda
как конструкцию, которую люди видели раньше в разных контекстах, поэтому другим, вероятно, будет легче читать и понимать.
Еще одно предостережение: ваша IDE должна иметь возможность сообщать опечатку в имени attr при использовании lambda
, в отличие от использования attrgetter
.
В общем, я склонен выбирать конструкцию, которая не требует дополнительного импорта, если альтернатива достаточно проста для записи и чтения.
Ответ 4
Это на самом деле ваше решение.
Я бы предпочел использовать attrgetter
потому что lambda
не очень чистая, и намного намного быстрее, посмотрите на время:
>>> timeit.timeit(lambda: seq.sort(key=operator.attrgetter('xxx')),number=10)
0.00013137529401774373
>>> timeit.timeit(lambda: seq.sort(key=lambda a:a.xxx),number=10)
5.952943013198819e-05
>>>
Так что это очень ясно, attrgetter
намного быстрее, без лямбды, намного чище.