¿Cómo puedo obtener un comportamiento de clasificación similar a 2.x en Python 3.x?
python-3.x sorting (10)
@ martijn-pieters No sé si la lista en python2 también tiene un
__cmp__
para manejar la comparación de objetos de lista o cómo se manejó en python2.
De todos modos, además de la respuesta de @ martijn-pieters , utilicé el siguiente comparador de listas, por lo que al menos no proporciona una salida ordenada diferente en función de un orden diferente de elementos en el mismo conjunto de entrada.
@per_type_cmp(list) def list_cmp(a, b): for a_item, b_item in zip(a, b): if a_item == b_item: continue return python2_sort_key(a_item) < python2_sort_key(b_item) return len(a) < len(b)
Entonces, uniéndolo con la respuesta original de Martijn:
from numbers import Number
# decorator for type to function mapping special cases
def per_type_cmp(type_):
try:
mapping = per_type_cmp.mapping
except AttributeError:
mapping = per_type_cmp.mapping = {}
def decorator(cmpfunc):
mapping[type_] = cmpfunc
return cmpfunc
return decorator
class python2_sort_key(object):
_unhandled_types = {complex}
def __init__(self, ob):
self._ob = ob
def __lt__(self, other):
_unhandled_types = self._unhandled_types
self, other = self._ob, other._ob # we don''t care about the wrapper
# default_3way_compare is used only if direct comparison failed
try:
return self < other
except TypeError:
pass
# hooks to implement special casing for types, dict in Py2 has
# a dedicated __cmp__ method that is gone in Py3 for example.
for type_, special_cmp in per_type_cmp.mapping.items():
if isinstance(self, type_) and isinstance(other, type_):
return special_cmp(self, other)
# explicitly raise again for types that won''t sort in Python 2 either
if type(self) in _unhandled_types:
raise TypeError(''no ordering relation is defined for {}''.format(
type(self).__name__))
if type(other) in _unhandled_types:
raise TypeError(''no ordering relation is defined for {}''.format(
type(other).__name__))
# default_3way_compare from Python 2 as Python code
# same type but no ordering defined, go by id
if type(self) is type(other):
return id(self) < id(other)
# None always comes first
if self is None:
return True
if other is None:
return False
# Sort by typename, but numbers are sorted before other types
self_tname = '''' if isinstance(self, Number) else type(self).__name__
other_tname = '''' if isinstance(other, Number) else type(other).__name__
if self_tname != other_tname:
return self_tname < other_tname
# same typename, or both numbers, but different type objects, order
# by the id of the type object
return id(type(self)) < id(type(other))
@per_type_cmp(dict)
def dict_cmp(a, b, _s=object()):
if len(a) != len(b):
return len(a) < len(b)
adiff = min((k for k in a if a[k] != b.get(k, _s)), key=python2_sort_key, default=_s)
if adiff is _s:
# All keys in a have a matching value in b, so the dicts are equal
return False
bdiff = min((k for k in b if b[k] != a.get(k, _s)), key=python2_sort_key)
if adiff != bdiff:
return python2_sort_key(adiff) < python2_sort_key(bdiff)
return python2_sort_key(a[adiff]) < python2_sort_key(b[bdiff])
@per_type_cmp(list)
def list_cmp(a, b):
for a_item, b_item in zip(a, b):
if a_item == b_item:
continue
return python2_sort_key(a_item) < python2_sort_key(b_item)
return len(a) < len(b)
PD: Tiene más sentido crearlo como un comentario, pero no tenía suficiente reputación para hacer un comentario. Entonces, lo estoy creando como una respuesta.
Estoy tratando de replicar (y si es posible mejorar) el comportamiento de clasificación de Python 2.x en 3.x, de modo que los tipos que se pueden ordenar mutuamente como
int
,
float
, etc. se ordenan como se esperaba, y los tipos que no se pueden ordenar mutuamente se agrupan dentro de la salida.
Aquí hay un ejemplo de lo que estoy hablando:
>>> sorted([0, ''one'', 2.3, ''four'', -5]) # Python 2.x
[-5, 0, 2.3, ''four'', ''one'']
>>> sorted([0, ''one'', 2.3, ''four'', -5]) # Python 3.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: str() < int()
Mi intento anterior de esto, usando una clase para el parámetro clave para
sorted()
(ver
¿Por qué esta clase clave para ordenar secuencias heterogéneas se comporta de manera extraña?
) Está fundamentalmente rota, porque su enfoque de
- Tratando de comparar valores, y
- Si eso falla, recurrir a la comparación de la representación de cadena de sus tipos
puede conducir a un orden intransitivo, como lo explica la excelente respuesta de BrenBarn .
Un enfoque ingenuo, que inicialmente rechacé sin siquiera intentar codificarlo, sería utilizar una función de tecla que devuelva una tupla
(type, value)
:
def motley(value):
return repr(type(value)), value
Sin embargo, esto no hace lo que quiero. En primer lugar, rompe el orden natural de los tipos que se pueden pedir mutuamente:
>>> sorted([0, 123.4, 5, -6, 7.89])
[-6, 0, 5, 7.89, 123.4]
>>> sorted([0, 123.4, 5, -6, 7.89], key=motley)
[7.89, 123.4, -6, 0, 5]
En segundo lugar, genera una excepción cuando la entrada contiene dos objetos del mismo tipo intrínsecamente no ordenable:
>>> sorted([{1:2}, {3:4}], key=motley)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: dict() < dict()
... que sin duda es el comportamiento estándar en Python 2.xy 3.x, pero idealmente me gustaría que estos tipos se agrupen (no me importa especialmente su orden, pero parece estar en consonancia con La garantía de Python de una clasificación estable de que conservan su orden original).
Puedo solucionar el primero de estos problemas para los tipos numéricos con mayúsculas especiales:
from numbers import Real
from decimal import Decimal
def motley(value):
numeric = Real, Decimal
if isinstance(value, numeric):
typeinfo = numeric
else:
typeinfo = type(value)
return repr(typeinfo), value
... que funciona hasta donde llega:
>>> sorted([0, ''one'', 2.3, ''four'', -5], key=motley)
[-5, 0, 2.3, ''four'', ''one'']
... pero no tiene en cuenta el hecho de que puede haber otros tipos distintos (posiblemente definidos por el usuario) que se pueden pedir mutuamente y, por supuesto, todavía falla con los tipos intrínsecamente no ordenados:
>>> sorted([{1:2}, {3:4}], key=motley)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: dict() < dict()
¿Existe otro enfoque que resuelva tanto el problema de los tipos arbitrarios, distintos pero mutuamente ordenables como el de los tipos intrínsecamente no ordenables?
Aquí hay un método para lograr esto:
lst = [0, ''one'', 2.3, ''four'', -5]
a=[x for x in lst if type(x) == type(1) or type(x) == type(1.1)]
b=[y for y in lst if type(y) == type(''string'')]
a.sort()
b.sort()
c = a+b
print(c)
Esta respuesta tiene como objetivo recrear fielmente el orden de clasificación de Python 2, en Python 3, en cada detalle.
La implementación real de Python 2 es bastante complicada, pero
default_3way_compare
object.c
el
object.c
final después de que las instancias hayan tenido la oportunidad de implementar reglas de comparación normales.
Esto es después de que los tipos individuales hayan tenido la oportunidad de comparar (a través de los ganchos
__cmp__
o
__lt__
).
Implementar esa función como Python puro en un contenedor, además de emular las excepciones a las reglas (específicamente
dict
y números complejos) nos da la misma semántica de clasificación de Python 2 en Python 3:
from numbers import Number
# decorator for type to function mapping special cases
def per_type_cmp(type_):
try:
mapping = per_type_cmp.mapping
except AttributeError:
mapping = per_type_cmp.mapping = {}
def decorator(cmpfunc):
mapping[type_] = cmpfunc
return cmpfunc
return decorator
class python2_sort_key(object):
_unhandled_types = {complex}
def __init__(self, ob):
self._ob = ob
def __lt__(self, other):
_unhandled_types = self._unhandled_types
self, other = self._ob, other._ob # we don''t care about the wrapper
# default_3way_compare is used only if direct comparison failed
try:
return self < other
except TypeError:
pass
# hooks to implement special casing for types, dict in Py2 has
# a dedicated __cmp__ method that is gone in Py3 for example.
for type_, special_cmp in per_type_cmp.mapping.items():
if isinstance(self, type_) and isinstance(other, type_):
return special_cmp(self, other)
# explicitly raise again for types that won''t sort in Python 2 either
if type(self) in _unhandled_types:
raise TypeError(''no ordering relation is defined for {}''.format(
type(self).__name__))
if type(other) in _unhandled_types:
raise TypeError(''no ordering relation is defined for {}''.format(
type(other).__name__))
# default_3way_compare from Python 2 as Python code
# same type but no ordering defined, go by id
if type(self) is type(other):
return id(self) < id(other)
# None always comes first
if self is None:
return True
if other is None:
return False
# Sort by typename, but numbers are sorted before other types
self_tname = '''' if isinstance(self, Number) else type(self).__name__
other_tname = '''' if isinstance(other, Number) else type(other).__name__
if self_tname != other_tname:
return self_tname < other_tname
# same typename, or both numbers, but different type objects, order
# by the id of the type object
return id(type(self)) < id(type(other))
@per_type_cmp(dict)
def dict_cmp(a, b, _s=object()):
if len(a) != len(b):
return len(a) < len(b)
adiff = min((k for k in a if a[k] != b.get(k, _s)), key=python2_sort_key, default=_s)
if adiff is _s:
# All keys in a have a matching value in b, so the dicts are equal
return False
bdiff = min((k for k in b if b[k] != a.get(k, _s)), key=python2_sort_key)
if adiff != bdiff:
return python2_sort_key(adiff) < python2_sort_key(bdiff)
return python2_sort_key(a[adiff]) < python2_sort_key(b[bdiff])
Incorporé el
manejo de la ordenación del diccionario como se implementó en Python 2
, ya que eso sería compatible con el tipo en sí a través de un
__cmp__
.
Me he adherido al pedido de Python 2 para las claves y los valores también, naturalmente.
También agregué una carcasa especial para números complejos, ya que Python 2 genera una excepción cuando intenta ordenarlos:
>>> sorted([0.0, 1, (1+0j), False, (2+3j)])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: no ordering relation is defined for complex numbers
Es posible que deba agregar más casos especiales si desea emular exactamente el comportamiento de Python 2.
Si de todos modos desea ordenar números complejos, deberá colocarlos consistentemente con el grupo que no sea de números; p.ej:
# Sort by typename, but numbers are sorted before other types
if isinstance(self, Number) and not isinstance(self, complex):
self_tname = ''''
else:
self_tname = type(self).__name__
if isinstance(other, Number) and not isinstance(other, complex):
other_tname = ''''
else:
other_tname = type(other).__name__
Algunos casos de prueba:
>>> sorted([0, ''one'', 2.3, ''four'', -5], key=python2_sort_key)
[-5, 0, 2.3, ''four'', ''one'']
>>> sorted([0, 123.4, 5, -6, 7.89], key=python2_sort_key)
[-6, 0, 5, 7.89, 123.4]
>>> sorted([{1:2}, {3:4}], key=python2_sort_key)
[{1: 2}, {3: 4}]
>>> sorted([{1:2}, None, {3:4}], key=python2_sort_key)
[None, {1: 2}, {3: 4}]
Idea estúpida: haga un primer paso para dividir todos los elementos diferentes en grupos que se pueden comparar entre sí, ordenar los grupos individuales y finalmente concatenarlos. Supongo que un elemento es comparable a todos los miembros de un grupo, si es comparable con el primer miembro de un grupo. Algo como esto (Python3):
import itertools
def python2sort(x):
it = iter(x)
groups = [[next(it)]]
for item in it:
for group in groups:
try:
item < group[0] # exception if not comparable
group.append(item)
break
except TypeError:
continue
else: # did not break, make new group
groups.append([item])
print(groups) # for debugging
return itertools.chain.from_iterable(sorted(group) for group in groups)
Esto tendrá un tiempo de ejecución cuadrático en el caso patético de que ninguno de los elementos es comparable, pero supongo que la única forma de saberlo con certeza es verificar todas las combinaciones posibles. Vea el comportamiento cuadrático como un castigo merecido para cualquiera que intente ordenar una larga lista de elementos no ordenables, como números complejos. En un caso más común de una mezcla de algunas cadenas y algunos enteros, la velocidad debería ser similar a la velocidad de un tipo normal. Examen rápido:
In [19]: x = [0, ''one'', 2.3, ''four'', -5, 1j, 2j, -5.5, 13 , 15.3, ''aa'', ''zz'']
In [20]: list(python2sort(x))
[[0, 2.3, -5, -5.5, 13, 15.3], [''one'', ''four'', ''aa'', ''zz''], [1j], [2j]]
Out[20]: [-5.5, -5, 0, 2.3, 13, 15.3, ''aa'', ''four'', ''one'', ''zz'', 1j, 2j]
Parece ser también un "tipo estable", ya que los grupos se forman en el orden en que se encuentran los elementos incomparables.
Me gustaría recomendar comenzar este tipo de tarea (como imitar el comportamiento de otro sistema muy cercano a este) con una aclaración detallada del sistema de destino. ¿Cómo debería funcionar con diferentes casos de esquina? Una de las mejores formas de hacerlo es escribir un montón de pruebas para garantizar un comportamiento correcto. Tener tales pruebas da:
- Comprenda mejor qué elementos deben preceder a qué
- Documentación básica
- Hace que el sistema sea robusto frente a algunas funciones de refactorización y adición. Por ejemplo, si se agrega una regla más, ¿cómo asegurarse de que las anteriores no se rompan?
Uno puede escribir tales casos de prueba:
sort2_test.py
import unittest
from sort2 import sorted2
class TestSortNumbers(unittest.TestCase):
"""
Verifies numbers are get sorted correctly.
"""
def test_sort_empty(self):
self.assertEqual(sorted2([]), [])
def test_sort_one_element_int(self):
self.assertEqual(sorted2([1]), [1])
def test_sort_one_element_real(self):
self.assertEqual(sorted2([1.0]), [1.0])
def test_ints(self):
self.assertEqual(sorted2([1, 2]), [1, 2])
def test_ints_reverse(self):
self.assertEqual(sorted2([2, 1]), [1, 2])
class TestSortStrings(unittest.TestCase):
"""
Verifies numbers are get sorted correctly.
"""
def test_sort_one_element_str(self):
self.assertEqual(sorted2(["1.0"]), ["1.0"])
class TestSortIntString(unittest.TestCase):
"""
Verifies numbers and strings are get sorted correctly.
"""
def test_string_after_int(self):
self.assertEqual(sorted2([1, "1"]), [1, "1"])
self.assertEqual(sorted2([0, "1"]), [0, "1"])
self.assertEqual(sorted2([-1, "1"]), [-1, "1"])
self.assertEqual(sorted2(["1", 1]), [1, "1"])
self.assertEqual(sorted2(["0", 1]), [1, "0"])
self.assertEqual(sorted2(["-1", 1]), [1, "-1"])
class TestSortIntDict(unittest.TestCase):
"""
Verifies numbers and dict are get sorted correctly.
"""
def test_string_after_int(self):
self.assertEqual(sorted2([1, {1: 2}]), [1, {1: 2}])
self.assertEqual(sorted2([0, {1: 2}]), [0, {1: 2}])
self.assertEqual(sorted2([-1, {1: 2}]), [-1, {1: 2}])
self.assertEqual(sorted2([{1: 2}, 1]), [1, {1: 2}])
self.assertEqual(sorted2([{1: 2}, 1]), [1, {1: 2}])
self.assertEqual(sorted2([{1: 2}, 1]), [1, {1: 2}])
El siguiente puede tener esa función de clasificación:
sort2.py
from numbers import Real
from decimal import Decimal
from itertools import tee, filterfalse
def sorted2(iterable):
"""
:param iterable: An iterable (array or alike)
entity which elements should be sorted.
:return: List with sorted elements.
"""
def predicate(x):
return isinstance(x, (Real, Decimal))
t1, t2 = tee(iterable)
numbers = filter(predicate, t1)
non_numbers = filterfalse(predicate, t2)
sorted_numbers = sorted(numbers)
sorted_non_numbers = sorted(non_numbers, key=str)
return sorted_numbers + sorted_non_numbers
El uso es bastante simple y está documentado en pruebas:
>>> from sort2 import sorted2
>>> sorted2([1,2,3, "aaa", {3:5}, [1,2,34], {-8:15}])
[1, 2, 3, [1, 2, 34], ''aaa'', {-8: 15}, {3: 5}]
No ejecuta Python 3 aquí, pero tal vez algo como esto funcione. Pruebe para ver si hacer un "menos que" comparar en "valor" crea una excepción y luego haga "algo" para manejar ese caso, como convertirlo en una cadena.
Por supuesto, aún necesitaría un manejo más especial si hay otros tipos en su lista que no son del mismo tipo pero que se pueden pedir mutuamente.
from numbers import Real
from decimal import Decimal
def motley(value):
numeric = Real, Decimal
if isinstance(value, numeric):
typeinfo = numeric
else:
typeinfo = type(value)
try:
x = value < value
except TypeError:
value = repr(value)
return repr(typeinfo), value
>>> print sorted([0, ''one'', 2.3, ''four'', -5, (2+3j), (1-3j)], key=motley)
[-5, 0, 2.3, (1-3j), (2+3j), ''four'', ''one'']
Para evitar el uso de excepciones y buscar una solución basada en tipos, se me ocurrió esto:
#! /usr/bin/python3
import itertools
def p2Sort(x):
notImpl = type(0j.__gt__(0j))
it = iter(x)
first = next(it)
groups = [[first]]
types = {type(first):0}
for item in it:
item_type = type(item)
if item_type in types.keys():
groups[types[item_type]].append(item)
else:
types[item_type] = len(types)
groups.append([item])
#debuggng
for group in groups:
print(group)
for it in group:
print(type(it),)
#
for i in range(len(groups)):
if type(groups[i][0].__gt__(groups[i][0])) == notImpl:
continue
groups[i] = sorted(groups[i])
return itertools.chain.from_iterable(group for group in groups)
x = [0j, ''one'', 2.3, ''four'', -5, 3j, 0j, -5.5, 13 , 15.3, ''aa'', ''zz'']
print(list(p2Sort(x)))
Tenga en cuenta que se necesita un diccionario adicional para contener los diferentes tipos en la lista y una variable de retención de tipos (notImpl). Nota adicional, que las carrozas y las entradas no se mezclan aquí.
Salida:
================================================================================
05.04.2017 18:27:57
~/Desktop/sorter.py
--------------------------------------------------------------------------------
[0j, 3j, 0j]
<class ''complex''>
<class ''complex''>
<class ''complex''>
[''one'', ''four'', ''aa'', ''zz'']
<class ''str''>
<class ''str''>
<class ''str''>
<class ''str''>
[2.3, -5.5, 15.3]
<class ''float''>
<class ''float''>
<class ''float''>
[-5, 13]
<class ''int''>
<class ''int''>
[0j, 3j, 0j, ''aa'', ''four'', ''one'', ''zz'', -5.5, 2.3, 15.3, -5, 13]
Podemos resolver este problema de la siguiente manera.
- Agrupar por tipo.
- Encuentre qué tipos son comparables intentando comparar un solo representante de cada tipo.
- Fusionar grupos de tipos comparables.
- Ordenar grupos combinados, si es posible.
- rendimiento de grupos fusionados (ordenados)
Podemos obtener una función de clave determinista y ordenable de los tipos mediante el uso de
repr(type(x))
.
Tenga en cuenta que la ''jerarquía de tipos'' aquí está determinada por la reproducción de los tipos mismos.
Una falla en este método es que si dos tipos tienen
__repr__
idénticos (los tipos mismos, no las instancias), ''confundirá'' los tipos.
Esto se puede resolver mediante el uso de una función de tecla que devuelve una tupla
(repr(type), id(type))
, pero no lo he implementado en esta solución.
La ventaja de mi método sobre Bas Swinkel es un manejo más limpio de un grupo de elementos que no se pueden ordenar. No tenemos comportamiento cuadrático; en cambio, la función se da por vencida después del primer intento de ordenar durante sorted ()).
Mi método funciona peor en el escenario donde hay un número extremadamente grande de tipos diferentes en el iterable. Este es un escenario raro, pero supongo que podría surgir.
def py2sort(iterable):
by_type_repr = lambda x: repr(type(x))
iterable = sorted(iterable, key = by_type_repr)
types = {type_: list(group) for type_, group in groupby(iterable, by_type_repr)}
def merge_compatible_types(types):
representatives = [(type_, items[0]) for (type_, items) in types.items()]
def mergable_types():
for i, (type_0, elem_0) in enumerate(representatives, 1):
for type_1, elem_1 in representatives[i:]:
if _comparable(elem_0, elem_1):
yield type_0, type_1
def merge_types(a, b):
try:
types[a].extend(types[b])
del types[b]
except KeyError:
pass # already merged
for a, b in mergable_types():
merge_types(a, b)
return types
def gen_from_sorted_comparable_groups(types):
for _, items in types.items():
try:
items = sorted(items)
except TypeError:
pass #unorderable type
yield from items
types = merge_compatible_types(types)
return list(gen_from_sorted_comparable_groups(types))
def _comparable(x, y):
try:
x < y
except TypeError:
return False
else:
return True
if __name__ == ''__main__'':
print(''before py2sort:'')
test = [2, -11.6, 3, 5.0, (1, ''5'', 3), (object, object()), complex(2, 3), [list, tuple], Fraction(11, 2), ''2'', type, str, ''foo'', object(), ''bar'']
print(test)
print(''after py2sort:'')
print(py2sort(test))
Traté de implementar el código de clasificación de Python 2 en python 3 con la mayor fidelidad posible.
Úselo así:
mydata.sort(key=py2key())
o
mydata.sort(key=py2key(lambda x: mykeyfunc))
def default_3way_compare(v, w): # Yes, this is how Python 2 sorted things :)
tv, tw = type(v), type(w)
if tv is tw:
return -1 if id(v) < id(w) else (1 if id(v) > id(w) else 0)
if v is None:
return -1
if w is None:
return 1
if isinstance(v, (int, float)):
vname = ''''
else:
vname = type(v).__name__
if isinstance(w, (int, float)):
wname = ''''
else:
wname = type(w).__name__
if vname < wname:
return -1
if vname > wname:
return 1
return -1 if id(type(v)) < id(type(w)) else 1
def py2key(func=None): # based on cmp_to_key
class K(object):
__slots__ = [''obj'']
__hash__ = None
def __init__(self, obj):
self.obj = func(obj) if func else obj
def __lt__(self, other):
try:
return self.obj < other.obj
except TypeError:
return default_3way_compare(self.obj, other.obj) < 0
def __gt__(self, other):
try:
return self.obj > other.obj
except TypeError:
return default_3way_compare(self.obj, other.obj) > 0
def __eq__(self, other):
try:
return self.obj == other.obj
except TypeError:
return default_3way_compare(self.obj, other.obj) == 0
def __le__(self, other):
try:
return self.obj <= other.obj
except TypeError:
return default_3way_compare(self.obj, other.obj) <= 0
def __ge__(self, other):
try:
return self.obj >= other.obj
except TypeError:
return default_3way_compare(self.obj, other.obj) >= 0
return K
Una forma para Python 3.2+ es usar
functools.cmp_to_key()
.
Con esto, puede implementar rápidamente una solución que intente comparar los valores y luego recurrir a la comparación de la representación de cadena de los tipos.
También puede evitar que se genere un error al comparar tipos desordenados y dejar el orden como en el caso original:
from functools import cmp_to_key
def cmp(a,b):
try:
return (a > b) - (a < b)
except TypeError:
s1, s2 = type(a).__name__, type(b).__name__
return (s1 > s2) - (s1 < s2)
Ejemplos (listas de entrada tomadas de la respuesta de Martijn Pieters ):
sorted([0, ''one'', 2.3, ''four'', -5], key=cmp_to_key(cmp))
# [-5, 0, 2.3, ''four'', ''one'']
sorted([0, 123.4, 5, -6, 7.89], key=cmp_to_key(cmp))
# [-6, 0, 5, 7.89, 123.4]
sorted([{1:2}, {3:4}], key=cmp_to_key(cmp))
# [{1: 2}, {3: 4}]
sorted([{1:2}, None, {3:4}], key=cmp_to_key(cmp))
# [None, {1: 2}, {3: 4}]
Esto tiene la desventaja de que la comparación de tres vías siempre se realiza, lo que aumenta la complejidad del tiempo.
Sin embargo, la solución es baja, corta, limpia y creo que
cmp_to_key()
se desarrolló para este tipo de caso de uso de emulación de Python 2.