Höchstwahrscheinlich functools.cmp_to_key()
hängt es eng mit der zugrunde liegenden Implementierung der Python-Sortierung zusammen. Außerdem ist der Parameter cmp ein Legacy. Die moderne Methode besteht darin, die Eingabeelemente in Objekte umzuwandeln, die die gewünschten umfangreichen Vergleichsoperationen unterstützen.
Unter CPython 2.x können Objekte unterschiedlicher Typen bestellt werden, auch wenn die jeweiligen Rich-Vergleichsoperatoren nicht implementiert wurden. Unter CPython 3.x müssen Objekte unterschiedlichen Typs den Vergleich explizit unterstützen. Siehe Wie vergleicht Python String und Int? die Links zur offiziellen Dokumentation . Die meisten Antworten hängen von dieser impliziten Reihenfolge ab. Für den Wechsel zu Python 3.x ist ein neuer Typ erforderlich, um Vergleiche zwischen Zahlen und Zeichenfolgen zu implementieren und zu vereinheitlichen.
Python 2.7.12 (default, Sep 29 2016, 13:30:34)
>>> (0,"foo") < ("foo",0)
True
Python 3.5.2 (default, Oct 14 2016, 12:54:53)
>>> (0,"foo") < ("foo",0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: int() < str()
Es gibt drei verschiedene Ansätze. Die erste verwendet verschachtelte Klassen, um den Iterable
Vergleichsalgorithmus von Python zu nutzen . Der zweite rollt diese Verschachtelung in eine einzelne Klasse ab. Die dritte verzichtet auf Unterklassen, str
um sich auf die Leistung zu konzentrieren. Alle sind zeitgesteuert; Der zweite ist doppelt so schnell, während der dritte fast sechsmal schneller ist. Unterklassen str
sind nicht erforderlich und waren wahrscheinlich an erster Stelle eine schlechte Idee, bringen jedoch gewisse Annehmlichkeiten mit sich.
Die Sortierzeichen werden dupliziert, um die Reihenfolge nach Groß- und Kleinschreibung zu erzwingen, und die Groß- und Kleinschreibung ausgetauscht, um zu erzwingen, dass Kleinbuchstaben zuerst sortiert werden. Dies ist die typische Definition von "natürlicher Sorte". Ich konnte mich nicht für die Art der Gruppierung entscheiden. Einige bevorzugen möglicherweise Folgendes, was ebenfalls erhebliche Leistungsvorteile mit sich bringt:
d = lambda s: s.lower()+s.swapcase()
Wo verwendet, werden die Vergleichsoperatoren auf die von eingestellt, object
damit sie von nicht ignoriert werdenfunctools.total_ordering
.
import functools
import itertools
@functools.total_ordering
class NaturalStringA(str):
def __repr__(self):
return "{}({})".format\
( type(self).__name__
, super().__repr__()
)
d = lambda c, s: [ c.NaturalStringPart("".join(v))
for k,v in
itertools.groupby(s, c.isdigit)
]
d = classmethod(d)
@functools.total_ordering
class NaturalStringPart(str):
d = lambda s: "".join(c.lower()+c.swapcase() for c in s)
d = staticmethod(d)
def __lt__(self, other):
if not isinstance(self, type(other)):
return NotImplemented
try:
return int(self) < int(other)
except ValueError:
if self.isdigit():
return True
elif other.isdigit():
return False
else:
return self.d(self) < self.d(other)
def __eq__(self, other):
if not isinstance(self, type(other)):
return NotImplemented
try:
return int(self) == int(other)
except ValueError:
if self.isdigit() or other.isdigit():
return False
else:
return self.d(self) == self.d(other)
__le__ = object.__le__
__ne__ = object.__ne__
__gt__ = object.__gt__
__ge__ = object.__ge__
def __lt__(self, other):
return self.d(self) < self.d(other)
def __eq__(self, other):
return self.d(self) == self.d(other)
__le__ = object.__le__
__ne__ = object.__ne__
__gt__ = object.__gt__
__ge__ = object.__ge__
import functools
import itertools
@functools.total_ordering
class NaturalStringB(str):
def __repr__(self):
return "{}({})".format\
( type(self).__name__
, super().__repr__()
)
d = lambda s: "".join(c.lower()+c.swapcase() for c in s)
d = staticmethod(d)
def __lt__(self, other):
if not isinstance(self, type(other)):
return NotImplemented
groups = map(lambda i: itertools.groupby(i, type(self).isdigit), (self, other))
zipped = itertools.zip_longest(*groups)
for s,o in zipped:
if s is None:
return True
if o is None:
return False
s_k, s_v = s[0], "".join(s[1])
o_k, o_v = o[0], "".join(o[1])
if s_k and o_k:
s_v, o_v = int(s_v), int(o_v)
if s_v == o_v:
continue
return s_v < o_v
elif s_k:
return True
elif o_k:
return False
else:
s_v, o_v = self.d(s_v), self.d(o_v)
if s_v == o_v:
continue
return s_v < o_v
return False
def __eq__(self, other):
if not isinstance(self, type(other)):
return NotImplemented
groups = map(lambda i: itertools.groupby(i, type(self).isdigit), (self, other))
zipped = itertools.zip_longest(*groups)
for s,o in zipped:
if s is None or o is None:
return False
s_k, s_v = s[0], "".join(s[1])
o_k, o_v = o[0], "".join(o[1])
if s_k and o_k:
s_v, o_v = int(s_v), int(o_v)
if s_v == o_v:
continue
return False
elif s_k or o_k:
return False
else:
s_v, o_v = self.d(s_v), self.d(o_v)
if s_v == o_v:
continue
return False
return True
__le__ = object.__le__
__ne__ = object.__ne__
__gt__ = object.__gt__
__ge__ = object.__ge__
import functools
import itertools
import enum
class OrderingType(enum.Enum):
PerWordSwapCase = lambda s: s.lower()+s.swapcase()
PerCharacterSwapCase = lambda s: "".join(c.lower()+c.swapcase() for c in s)
class NaturalOrdering:
@classmethod
def by(cls, ordering):
def wrapper(string):
return cls(string, ordering)
return wrapper
def __init__(self, string, ordering=OrderingType.PerCharacterSwapCase):
self.string = string
self.groups = [ (k,int("".join(v)))
if k else
(k,ordering("".join(v)))
for k,v in
itertools.groupby(string, str.isdigit)
]
def __repr__(self):
return "{}({})".format\
( type(self).__name__
, self.string
)
def __lesser(self, other, default):
if not isinstance(self, type(other)):
return NotImplemented
for s,o in itertools.zip_longest(self.groups, other.groups):
if s is None:
return True
if o is None:
return False
s_k, s_v = s
o_k, o_v = o
if s_k and o_k:
if s_v == o_v:
continue
return s_v < o_v
elif s_k:
return True
elif o_k:
return False
else:
if s_v == o_v:
continue
return s_v < o_v
return default
def __lt__(self, other):
return self.__lesser(other, default=False)
def __le__(self, other):
return self.__lesser(other, default=True)
def __eq__(self, other):
if not isinstance(self, type(other)):
return NotImplemented
for s,o in itertools.zip_longest(self.groups, other.groups):
if s is None or o is None:
return False
s_k, s_v = s
o_k, o_v = o
if s_k and o_k:
if s_v == o_v:
continue
return False
elif s_k or o_k:
return False
else:
if s_v == o_v:
continue
return False
return True
# functools.total_ordering doesn't create single-call wrappers if both
# __le__ and __lt__ exist, so do it manually.
def __gt__(self, other):
op_result = self.__le__(other)
if op_result is NotImplemented:
return op_result
return not op_result
def __ge__(self, other):
op_result = self.__lt__(other)
if op_result is NotImplemented:
return op_result
return not op_result
# __ne__ is the only implied ordering relationship, it automatically
# delegates to __eq__
>>> import natsort
>>> import timeit
>>> l1 = ['Apple', 'corn', 'apPlE', 'arbour', 'Corn', 'Banana', 'apple', 'banana']
>>> l2 = list(map(str, range(30)))
>>> l3 = ["{} {}".format(x,y) for x in l1 for y in l2]
>>> print(timeit.timeit('sorted(l3+["0"], key=NaturalStringA)', number=10000, globals=globals()))
362.4729259099986
>>> print(timeit.timeit('sorted(l3+["0"], key=NaturalStringB)', number=10000, globals=globals()))
189.7340817489967
>>> print(timeit.timeit('sorted(l3+["0"], key=NaturalOrdering.by(OrderingType.PerCharacterSwapCase))', number=10000, globals=globals()))
69.34636392899847
>>> print(timeit.timeit('natsort.natsorted(l3+["0"], alg=natsort.ns.GROUPLETTERS | natsort.ns.LOWERCASEFIRST)', number=10000, globals=globals()))
98.2531585780016
Natürliche Sortierung ist sowohl ziemlich kompliziert als auch vage als Problem definiert. Vergessen Sie nicht , laufen unicodedata.normalize(...)
vorher, und betrachten den Einsatz str.casefold()
statt str.lower()
. Es gibt wahrscheinlich subtile Codierungsprobleme, die ich nicht berücksichtigt habe. Daher empfehle ich vorläufig die Natsort- Bibliothek. Ich warf einen kurzen Blick auf das Github-Repository. Die Code-Wartung war hervorragend.
Alle Algorithmen, die ich gesehen habe, hängen von Tricks ab, wie dem Duplizieren und Verringern von Zeichen und dem Vertauschen von Groß- und Kleinschreibung. Während dies die Laufzeit verdoppelt, würde eine Alternative eine vollständige natürliche Reihenfolge des Eingabezeichensatzes erfordern. Ich denke nicht, dass dies Teil der Unicode-Spezifikation ist, und da es viel mehr Unicode-Ziffern als gibt [0-9]
, wäre das Erstellen einer solchen Sortierung ebenso entmutigend. Wenn Sie Vergleiche mit Gebietsschema wünschen, bereiten Sie Ihre Zeichenfolgen locale.strxfrm
gemäß Pythons Sortieranleitung vor .