Rất có thể functools.cmp_to_key()
được liên kết chặt chẽ với việc thực hiện cơ bản của loại trăn. Bên cạnh đó, tham số cmp là di sản. Cách hiện đại là biến đổi các mục đầu vào thành các đối tượng hỗ trợ các hoạt động so sánh phong phú mong muốn.
Theo CPython 2.x, các đối tượng của các loại khác nhau có thể được đặt hàng ngay cả khi các toán tử so sánh phong phú tương ứng chưa được triển khai. Theo CPython 3.x, các đối tượng thuộc các loại khác nhau phải hỗ trợ rõ ràng cho việc so sánh. Xem Python so sánh chuỗi và int như thế nào? có liên kết đến các tài liệu chính thức . Hầu hết các câu trả lời phụ thuộc vào thứ tự ngầm này. Chuyển sang Python 3.x sẽ yêu cầu một loại mới để thực hiện và thống nhất so sánh giữa các số và chuỗi.
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()
Có ba cách tiếp cận khác nhau. Đầu tiên sử dụng các lớp lồng nhau để tận dụng Iterable
thuật toán so sánh của Python . Thứ hai bỏ điều này lồng vào một lớp duy nhất. Nhóm thứ ba bỏ qua phân lớp str
để tập trung vào hiệu suất. Tất cả đều được tính thời gian; cái thứ hai nhanh gấp đôi trong khi cái thứ ba nhanh hơn gần sáu lần. Phân lớp str
không bắt buộc, và có lẽ là một ý tưởng tồi ở nơi đầu tiên, nhưng nó đi kèm với một số tiện lợi nhất định.
Các ký tự sắp xếp được nhân đôi để buộc thứ tự theo trường hợp và hoán đổi trường hợp để buộc chữ thường viết thường sắp xếp trước; đây là định nghĩa điển hình của "sắp xếp tự nhiên". Tôi không thể quyết định loại nhóm; một số có thể thích những điều sau đây, cũng mang lại lợi ích hiệu suất đáng kể:
d = lambda s: s.lower()+s.swapcase()
Khi được sử dụng, các toán tử so sánh được đặt thành đó object
để chúng không bị bỏ quafunctools.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
Sắp xếp tự nhiên là khá phức tạp và mơ hồ được xác định là một vấn đề. Đừng quên chạy unicodedata.normalize(...)
trước và xem xét sử dụng str.casefold()
hơn là str.lower()
. Có lẽ có những vấn đề mã hóa tinh tế mà tôi chưa xem xét. Vì vậy, tôi tạm thời đề nghị thư viện natsort . Tôi lướt qua kho lưu trữ github; việc bảo trì mã đã được xuất sắc.
Tất cả các thuật toán tôi đã thấy phụ thuộc vào các thủ thuật như sao chép và hạ thấp ký tự và trường hợp hoán đổi. Trong khi điều này tăng gấp đôi thời gian chạy, một sự thay thế sẽ yêu cầu tổng thứ tự tự nhiên trên bộ ký tự đầu vào. Tôi không nghĩ rằng đây là một phần của đặc tả unicode và vì có nhiều chữ số unicode hơn [0-9]
, việc tạo ra một cách sắp xếp như vậy sẽ gây khó khăn không kém. Nếu bạn muốn so sánh nhận biết miền địa phương, hãy chuẩn bị các chuỗi của bạn với locale.strxfrm
mỗi CÁCH Sắp xếp của Python .