lambda vs. operator.attrgetter (‘xxx’) como función de ordenamiento en Python

Estoy viendo un código que tiene muchas llamadas de tipo usando funciones de comparación, y parece que debería estar usando funciones clave.

Si seq.sort(lambda x,y: cmp(x.xxx, y.xxx)) cambiar seq.sort(lambda x,y: cmp(x.xxx, y.xxx)) , lo cual es preferible:

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

o:

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

También me interesaría recibir comentarios sobre los méritos de realizar cambios en el código existente que funcione.

Cuando se elige puramente entre attrgetter('attributename') y lambda o: o.attributename como clave de clasificación, usar attrgetter() es la opción más rápida de las dos.

Recuerde que la función clave solo se aplica una vez a cada elemento de la lista, antes de ordenar, por lo que para comparar los dos podemos usarlos directamente en una prueba de tiempo:

 >>> 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) 

Por lo tanto, aplicar attrgetter('bar') 1000 veces es aproximadamente 40μs más rápido que un lambda . Esto se debe a que llamar a una función de Python tiene una cierta cantidad de sobrecarga, más que llamar a una función nativa como la producida por attrgetter() .

Esta ventaja de velocidad se traduce también en una clasificación más rápida:

 >>> 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) 

“Hacer cambios en el código existente que funciona” es cómo evolucionan los progtwigs ;-). Escriba una buena batería de pruebas que den resultados conocidos con el código existente, guarde esos resultados (que normalmente se conocen como “archivos dorados” en un contexto de prueba); luego realice los cambios, vuelva a ejecutar las pruebas y verifique (idealmente de manera automatizada) que los únicos cambios en los resultados de las pruebas son aquellos que están específicamente destinados a estar allí, sin efectos secundarios no deseados o inesperados. Uno puede usar estrategias de control de calidad más sofisticadas, por supuesto, pero esta es la esencia de muchos enfoques de “prueba de integración”.

En cuanto a las dos formas de escribir simple key= function, la intención del diseño era hacer que operator.attrgetter fuera más rápido al ser más especializado, pero al menos en las versiones actuales de Python no hay una diferencia apreciable en la velocidad. Siendo ese el caso, para esta situación especial, recomendaría el lambda , simplemente porque es más conciso y general (y, por lo general, no soy un amante de la lambda, ¡fíjate!).