Czy jest wbudowana funkcja sortowania łańcucha naturalnego?

281

Korzystając z języka Python 3.x, mam listę ciągów znaków, dla których chciałbym wykonać naturalne sortowanie alfabetyczne.

Naturalne sortowanie: kolejność sortowania plików w systemie Windows.

Na przykład poniższa lista jest naturalnie posortowana (co chcę):

['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

A oto „posortowana” wersja powyższej listy (co mam):

['Elm11', 'Elm12', 'Elm2', 'elm0', 'elm1', 'elm10', 'elm13', 'elm9']

Szukam funkcji sortowania, która zachowuje się jak pierwsza.

snakile
źródło
13
Definicja sortowania naturalnego nie jest „kolejnością sortowania plików przez system Windows”.
Glenn Maynard,
Wszystkie odpowiedzi na tej stronie przyniosą niepoprawne wyniki, jeśli chcesz sortować w podobny sposób jak Eksplorator Windows w kilku przypadkach, na przykład sortowanie !1, 1, !a, a. Jedynym sposobem na uzyskanie sortowania takiego jak Windows wydaje się być użycie StrCmpLogicalW samej funkcji Windows , ponieważ wydaje się, że nikt nie zaimplementował tej funkcji poprawnie (źródło byłoby mile widziane). Rozwiązanie: stackoverflow.com/a/48030307/2441026
user136036

Odpowiedzi:

235

W PyPI dostępna jest biblioteka innej firmy o nazwie natsort (pełne ujawnienie, jestem autorem pakietu). W twoim przypadku możesz wykonać jedną z następujących czynności:

>>> from natsort import natsorted, ns
>>> x = ['Elm11', 'Elm12', 'Elm2', 'elm0', 'elm1', 'elm10', 'elm13', 'elm9']
>>> natsorted(x, key=lambda y: y.lower())
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
>>> natsorted(x, alg=ns.IGNORECASE)  # or alg=ns.IC
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

Należy zauważyć, że natsortużywa on ogólnego algorytmu, więc powinien on działać dla niemal wszystkich wprowadzanych do niego danych wejściowych. Jeśli chcesz uzyskać więcej informacji na temat tego, dlaczego możesz wybrać bibliotekę, aby to zrobić, a nie uruchamiać własną funkcję, sprawdź stronę natsortdokumentacyjną Jak to działa , w szczególności specjalne przypadki wszędzie! Sekcja.


Jeśli potrzebujesz klucza sortowania zamiast funkcji sortowania, użyj jednej z poniższych formuł.

>>> from natsort import natsort_keygen, ns
>>> l1 = ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
>>> l2 = l1[:]
>>> natsort_key1 = natsort_keygen(key=lambda y: y.lower())
>>> l1.sort(key=natsort_key1)
>>> l1
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
>>> natsort_key2 = natsort_keygen(alg=ns.IGNORECASE)
>>> l2.sort(key=natsort_key2)
>>> l2
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
SethMMorton
źródło
5
Myślę też, że to całkiem interesujące, że natsort sortuje również poprawne, gdy liczba nie jest na końcu: jak to często bywa w przypadku nazw plików. Dołącz następujący przykład: pastebin.com/9cwCLdEK
Martin Thoma
1
Natsort to świetna biblioteka, którą należy dodać do standardowej biblioteki Pythona! :-)
Mitch McMabers
natsortrównież „naturalnie” obsługuje przypadek wielu oddzielnych liczb w ciągach. Świetne rzeczy!
FlorianH
182

Spróbuj tego:

import re

def natural_sort(l): 
    convert = lambda text: int(text) if text.isdigit() else text.lower() 
    alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] 
    return sorted(l, key = alphanum_key)

Wynik:

['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

Kod zaadaptowany stąd: Sortowanie dla ludzi: Naturalny porządek sortowania .

Mark Byers
źródło
2
dlaczego używasz return sorted(l, key)zamiast l.sort(key)? Czy ma to na celu zwiększenie wydajności, czy po prostu bycie bardziej pytonicznym?
jperelli,
12
@jperelli Myślę, że drabina zmieniłaby oryginalną listę dzwoniącego. Ale najprawdopodobniej dzwoniący chce kolejnej płytkiej kopii listy.
huggie
3
Dla przypomnienia, nie może obsłużyć wszystkich danych wejściowych: podziały str / int muszą być wyrównane, w przeciwnym razie utworzysz porównania takie jak [„foo”, 0] <[0, „foo”] dla danych wejściowych [„foo0 ”,„ 0foo ”], który podnosi błąd typu.
user19087
4
@ user19087: W rzeczywistości to działa, ponieważ re.split('([0-9]+)', '0foo')zwraca ['', '0', 'foo']. Z tego powodu ciągi zawsze będą znajdować się na indeksach parzystych i liczbach całkowitych na indeksach nieparzystych w tablicy.
Florian Kusche
Dla każdego, kto zastanawia się nad wydajnością, jest to znacznie wolniejsze niż rodzime sortowanie Pythona. tj. 25 -50x wolniej. A jeśli zawsze chcesz niezawodnie sortować [elm1, elm2, Elm2, elm2] jako [elm1, Elm2, elm2, elm2] niezawodnie (wielkie litery przed dolnym), możesz po prostu wywołać natural_sort (sortowane (pierwsze)). Bardziej nieefektywne, ale bardzo łatwe do uzyskania powtarzalnego rodzaju. Skompiluj wyrażenie regularne dla przyspieszenia o ~ 50%. jak widać w odpowiedzi Claudiu.
Charlie Haley,
100

Oto znacznie bardziej pytoniczna wersja odpowiedzi Marka Byera:

import re

def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
    return [int(text) if text.isdigit() else text.lower()
            for text in _nsre.split(s)]    

Teraz ta funkcja może być używana jako klucz w każdej funkcji, która go używa, jak list.sort, sorted, max, itd.

Jako lambda:

lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]
Claudiu
źródło
9
moduł re kompiluje i buforuje wyrażenia regularne automatycznie, więc nie ma potrzeby wstępnej kompilacji
wim
1
@ wim: buforuje ostatnie użycie X, więc technicznie możliwe jest użycie wyrażeń regularnych X + 5, a następnie wielokrotne sortowanie naturalne, w którym to momencie nie będzie buforowane. ale prawdopodobnie nieistotne w dłuższej perspektywie
Claudiu
Nie zrobiłem tego, ale być może powodem było to, że nie jest w stanie poradzić sobie z krotkami, jak w przypadku zwykłego rodzaju python.
The Unfun Cat
1
Zastosowania X wspomniane przez @Claudiu wydają się mieć 100 w Pythonie 2.7 i 512 w Pythonie 3.4. Pamiętaj też, że po osiągnięciu limitu pamięć podręczna jest całkowicie czyszczona (więc nie tylko ta najstarsza jest wyrzucana).
Zitrax,
@Zitrax Dlaczego / Jak sensowne jest całkowite wyczyszczenie pamięci podręcznej?
Joschua
19

Napisałem funkcję opartą na http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html, która dodaje możliwość przekazywania własnego parametru „klucza”. Potrzebuję tego, aby wykonać naturalny rodzaj list, które zawierają bardziej złożone obiekty (nie tylko ciągi znaków).

import re

def natural_sort(list, key=lambda s:s):
    """
    Sort the list into natural alphanumeric order.
    """
    def get_alphanum_key_func(key):
        convert = lambda text: int(text) if text.isdigit() else text 
        return lambda s: [convert(c) for c in re.split('([0-9]+)', key(s))]
    sort_key = get_alphanum_key_func(key)
    list.sort(key=sort_key)

Na przykład:

my_list = [{'name':'b'}, {'name':'10'}, {'name':'a'}, {'name':'1'}, {'name':'9'}]
natural_sort(my_list, key=lambda x: x['name'])
print my_list
[{'name': '1'}, {'name': '9'}, {'name': '10'}, {'name': 'a'}, {'name': 'b'}]
beauburrier
źródło
prostszym sposobem na to byłoby zdefiniowanie natural_sort_key, a następnie podczas sortowania listy można łańcuchowo połączyć klucze, np .:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu
17
data = ['elm13', 'elm9', 'elm0', 'elm1', 'Elm11', 'Elm2', 'elm10']

Przeanalizujmy dane. Pojemność cyfrowa wszystkich elementów wynosi 2. I są 3 litery we wspólnej części dosłownej 'elm'.

Zatem maksymalna długość elementu wynosi 5. Możemy zwiększyć tę wartość, aby się upewnić (na przykład do 8).

Mając to na uwadze, mamy jedno-liniowe rozwiązanie:

data.sort(key=lambda x: '{0:0>8}'.format(x).lower())

bez wyrażeń regularnych i bibliotek zewnętrznych!

print(data)

>>> ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'elm13']

Wyjaśnienie:

for elm in data:
    print('{0:0>8}'.format(elm).lower())

>>>
0000elm0
0000elm1
0000elm2
0000elm9
000elm10
000elm11
000elm13
SergO
źródło
1
To nie obsługuje danych dynamicznych / nieznanych długości. Sortuje również inaczej niż inne rozwiązania dla danych, które mają liczby w danych przeciwnych na końcu. * To niekoniecznie jest niepożądane, ale myślę, że warto to podkreślić.
JerodG
1
Jeśli potrzebujesz obsługi danych o dynamicznej długości, możesz użyć width = max(data, key=len)do obliczenia tego, co wpisać w 8powyższe, a następnie '{0:0>{width}}'.format(x, width=width)
wpisać w
1
Po prostu wykonując test czasowy w porównaniu ze wszystkimi innymi na tym forum, to rozwiązanie jest zdecydowanie najszybsze i najbardziej wydajne dla danych, które @snakile próbuje przetworzyć
SR Colledge
13

Dany:

data=['Elm11', 'Elm12', 'Elm2', 'elm0', 'elm1', 'elm10', 'elm13', 'elm9']

Podobnie do rozwiązania SergO, 1-liniowy bez zewnętrznych bibliotek to :

data.sort(key=lambda x : int(x[3:]))

lub

sorted_data=sorted(data, key=lambda x : int(x[3:]))

Wyjaśnienie:

W tym rozwiązaniu kluczową funkcją sortowania jest zdefiniowanie funkcji, która zostanie zastosowana do sortowania. Ponieważ wiemy, że każde wprowadzanie danych poprzedza „wiąz”, funkcja sortująca konwertuje na liczbę całkowitą część ciągu po 3. znaku (tj. Int (x [3:])). Jeśli część liczbowa danych znajduje się w innym miejscu, wówczas ta część funkcji musiałaby się zmienić.

Twoje zdrowie

Camilo
źródło
6
A teraz coś bardziej * eleganckiego (pytonicznego) - wystarczy dotknięcie

Istnieje wiele implementacji i chociaż niektóre się zbliżyły, żadne nie oddało elegancji, jaką zapewnia współczesny python.

  • Testowane przy użyciu Pythona (3.5.1)
  • Zawiera dodatkową listę, aby wykazać, że działa, gdy liczby są w połowie łańcucha
  • Nie testowałem, jednak zakładam, że jeśli twoja lista była spora, bardziej efektywne byłoby wcześniejsze skompilowanie wyrażenia regularnego
    • Jestem pewien, że ktoś mnie poprawi, jeśli jest to błędne założenie

Szybko
from re import compile, split    
dre = compile(r'(\d+)')
mylist.sort(key=lambda l: [int(s) if s.isdigit() else s.lower() for s in split(dre, l)])
Pełny kod
#!/usr/bin/python3
# coding=utf-8
"""
Natural-Sort Test
"""

from re import compile, split

dre = compile(r'(\d+)')
mylist = ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13', 'elm']
mylist2 = ['e0lm', 'e1lm', 'E2lm', 'e9lm', 'e10lm', 'E12lm', 'e13lm', 'elm', 'e01lm']

mylist.sort(key=lambda l: [int(s) if s.isdigit() else s.lower() for s in split(dre, l)])
mylist2.sort(key=lambda l: [int(s) if s.isdigit() else s.lower() for s in split(dre, l)])

print(mylist)  
  # ['elm', 'elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
print(mylist2)  
  # ['e0lm', 'e1lm', 'e01lm', 'E2lm', 'e9lm', 'e10lm', 'E12lm', 'e13lm', 'elm']

Ostrożnie podczas korzystania

  • from os.path import split
    • będziesz musiał różnicować import

Inspiracja z

JerodG
źródło
6

Wartość tego postu

Chodzi mi o to, aby zaoferować rozwiązanie niebędące wyrażeniami regularnymi, które można zastosować ogólnie.
Stworzę trzy funkcje:

  1. find_first_digitktóre pożyczyłem od @AnuragUniyal . Znajduje pozycję pierwszej cyfry lub cyfry w ciągu.
  2. split_digitsktóry jest generatorem, który rozdziela ciąg na części cyfrowe i niecyfrowe. Będzie także yieldliczbą całkowitą, gdy będzie cyfrą.
  3. natural_keypo prostu owija split_digitssię w tuple. To, co możemy użyć jako klucza sorted, max, min.

Funkcje

def find_first_digit(s, non=False):
    for i, x in enumerate(s):
        if x.isdigit() ^ non:
            return i
    return -1

def split_digits(s, case=False):
    non = True
    while s:
        i = find_first_digit(s, non)
        if i == 0:
            non = not non
        elif i == -1:
            yield int(s) if s.isdigit() else s if case else s.lower()
            s = ''
        else:
            x, s = s[:i], s[i:]
            yield int(x) if x.isdigit() else x if case else x.lower()

def natural_key(s, *args, **kwargs):
    return tuple(split_digits(s, *args, **kwargs))

Widzimy, że ogólnie rzecz biorąc, możemy mieć wielocyfrowe fragmenty:

# Note that the key has lower case letters
natural_key('asl;dkfDFKJ:sdlkfjdf809lkasdjfa_543_hh')

('asl;dkfdfkj:sdlkfjdf', 809, 'lkasdjfa_', 543, '_hh')

Lub pozostaw jako wielkość liter:

natural_key('asl;dkfDFKJ:sdlkfjdf809lkasdjfa_543_hh', True)

('asl;dkfDFKJ:sdlkfjdf', 809, 'lkasdjfa_', 543, '_hh')

Widzimy, że sortuje listę PO w odpowiedniej kolejności

sorted(
    ['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13'],
    key=natural_key
)

['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

Ale może obsługiwać również bardziej skomplikowane listy:

sorted(
    ['f_1', 'e_1', 'a_2', 'g_0', 'd_0_12:2', 'd_0_1_:2'],
    key=natural_key
)

['a_2', 'd_0_1_:2', 'd_0_12:2', 'e_1', 'f_1', 'g_0']

Mój odpowiednik wyrażenia regularnego to

def int_maybe(x):
    return int(x) if str(x).isdigit() else x

def split_digits_re(s, case=False):
    parts = re.findall('\d+|\D+', s)
    if not case:
        return map(int_maybe, (x.lower() for x in parts))
    else:
        return map(int_maybe, parts)
    
def natural_key_re(s, *args, **kwargs):
    return tuple(split_digits_re(s, *args, **kwargs))
piRSquared
źródło
1
Wielkie dzięki! Chcę jednak dodać, że jeśli masz „12345_A” i „12345_A2”, te ostatnie zostaną posortowane przed pierwszym. Tak przynajmniej nie robi Windows. Nadal jednak działa na powyższy problem!
morph3us
4

Jedną z opcji jest przekształcenie łańcucha w krotkę i zastąpienie cyfr za pomocą rozszerzonej formy http://wiki.answers.com/Q/What_does_expanded_form_mean

w ten sposób a90 stałoby się („a”, 90,0), a a1 stałoby się („a”, 1)

poniżej znajduje się przykładowy kod (który nie jest zbyt wydajny ze względu na sposób, w jaki usuwa wiodące zera z liczb)

alist=["something1",
    "something12",
    "something17",
    "something2",
    "something25and_then_33",
    "something25and_then_34",
    "something29",
    "beta1.1",
    "beta2.3.0",
    "beta2.33.1",
    "a001",
    "a2",
    "z002",
    "z1"]

def key(k):
    nums=set(list("0123456789"))
        chars=set(list(k))
    chars=chars-nums
    for i in range(len(k)):
        for c in chars:
            k=k.replace(c+"0",c)
    l=list(k)
    base=10
    j=0
    for i in range(len(l)-1,-1,-1):
        try:
            l[i]=int(l[i])*base**j
            j+=1
        except:
            j=0
    l=tuple(l)
    print l
    return l

print sorted(alist,key=key)

wynik:

('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 1)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 10, 2)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 10, 7)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 2)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 20, 5, 'a', 'n', 'd', '_', 't', 'h', 'e', 'n', '_', 30, 3)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 20, 5, 'a', 'n', 'd', '_', 't', 'h', 'e', 'n', '_', 30, 4)
('s', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g', 20, 9)
('b', 'e', 't', 'a', 1, '.', 1)
('b', 'e', 't', 'a', 2, '.', 3, '.')
('b', 'e', 't', 'a', 2, '.', 30, 3, '.', 1)
('a', 1)
('a', 2)
('z', 2)
('z', 1)
['a001', 'a2', 'beta1.1', 'beta2.3.0', 'beta2.33.1', 'something1', 'something2', 'something12', 'something17', 'something25and_then_33', 'something25and_then_34', 'something29', 'z1', 'z002']
Robert King
źródło
1
Niestety, to rozwiązanie działa tylko dla Python 2.X. W przypadku Python 3 ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)powróciTypeError: unorderable types: int() < str()
SethMMorton
@SethMMorgon ma rację, ten kod łatwo przerwy w Pythonie 3. naturalną alternatywą wydaje się natsort, pypi.org/project/natsort
FlorianH
3

Na podstawie odpowiedzi tutaj napisałem natural_sortedfunkcję, która zachowuje się jak funkcja wbudowana sorted:

# Copyright (C) 2018, Benjamin Drung <[email protected]>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import re

def natural_sorted(iterable, key=None, reverse=False):
    """Return a new naturally sorted list from the items in *iterable*.

    The returned list is in natural sort order. The string is ordered
    lexicographically (using the Unicode code point number to order individual
    characters), except that multi-digit numbers are ordered as a single
    character.

    Has two optional arguments which must be specified as keyword arguments.

    *key* specifies a function of one argument that is used to extract a
    comparison key from each list element: ``key=str.lower``.  The default value
    is ``None`` (compare the elements directly).

    *reverse* is a boolean value.  If set to ``True``, then the list elements are
    sorted as if each comparison were reversed.

    The :func:`natural_sorted` function is guaranteed to be stable. A sort is
    stable if it guarantees not to change the relative order of elements that
    compare equal --- this is helpful for sorting in multiple passes (for
    example, sort by department, then by salary grade).
    """
    prog = re.compile(r"(\d+)")

    def alphanum_key(element):
        """Split given key in list of strings and digits"""
        return [int(c) if c.isdigit() else c for c in prog.split(key(element)
                if key else element)]

    return sorted(iterable, key=alphanum_key, reverse=reverse)

Kod źródłowy jest również dostępny w moim repozytorium fragmentów GitHub: https://github.com/bdrung/snippets/blob/master/natural_sorted.py

Benjamin Drung
źródło
2

Powyższe odpowiedzi są dobre dla konkretnego przykładu, który został pokazany, ale brakuje kilku przydatnych przypadków bardziej ogólnego pytania rodzaju naturalnego. Właśnie dostałem kawałek jednego z tych przypadków, więc stworzyłem dokładniejsze rozwiązanie:

def natural_sort_key(string_or_number):
    """
    by Scott S. Lawton <[email protected]> 2014-12-11; public domain and/or CC0 license

    handles cases where simple 'int' approach fails, e.g.
        ['0.501', '0.55'] floating point with different number of significant digits
        [0.01, 0.1, 1]    already numeric so regex and other string functions won't work (and aren't required)
        ['elm1', 'Elm2']  ASCII vs. letters (not case sensitive)
    """

    def try_float(astring):
        try:
            return float(astring)
        except:
            return astring

    if isinstance(string_or_number, basestring):
        string_or_number = string_or_number.lower()

        if len(re.findall('[.]\d', string_or_number)) <= 1:
            # assume a floating point value, e.g. to correctly sort ['0.501', '0.55']
            # '.' for decimal is locale-specific, e.g. correct for the Anglosphere and Asia but not continental Europe
            return [try_float(s) for s in re.split(r'([\d.]+)', string_or_number)]
        else:
            # assume distinct fields, e.g. IP address, phone number with '.', etc.
            # caveat: might want to first split by whitespace
            # TBD: for unicode, replace isdigit with isdecimal
            return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_or_number)]
    else:
        # consider: add code to recurse for lists/tuples and perhaps other iterables
        return string_or_number

Kod testowy i kilka linków (włączanie i wyłączanie StackOverflow) znajduje się tutaj: http://productarchitect.com/code/better-natural-sort.py

Witamy mile widziane. To nie ma być ostateczne rozwiązanie; tylko krok do przodu.

Scott Lawton
źródło
W skrypcie testowym, z którym się łączysz, natsortedale humansortednie powiodło się, ponieważ zostały użyte niepoprawnie ... próbowałeś przekazać natsortedjako klucz, ale tak naprawdę sama funkcja sortowania. Powinieneś był spróbować natsort_keygen().
SethMorton
2

Najprawdopodobniej functools.cmp_to_key()jest ściśle związany z podstawową implementacją sortowania Pythona. Poza tym parametr cmp jest starszy. Współczesny sposób polega na przekształceniu elementów wejściowych w obiekty, które obsługują pożądane operacje porównywania bogatego.

W CPython 2.x obiekty o różnych typach mogą być zamawiane, nawet jeśli odpowiednie operatory porównania bogatego nie zostały zaimplementowane. W CPython 3.x obiekty różnych typów muszą jawnie obsługiwać porównanie. Zobacz Jak Python porównuje ciąg znaków i int? który prowadzi do oficjalnej dokumentacji . Większość odpowiedzi zależy od tego dorozumianego uporządkowania. Przejście na Python 3.x będzie wymagało nowego typu do implementacji i ujednolicenia porównań między liczbami i łańcuchami.

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

Istnieją trzy różne podejścia. Pierwszy wykorzystuje zagnieżdżone klasy, aby skorzystać z Iterablealgorytmu porównawczego Pythona . Drugi rozwija to zagnieżdżenie w jedną klasę. Trzecia rezygnuje z podklas, straby skupić się na wydajności. Wszystkie są na czas; drugi jest dwa razy szybszy, a trzeci prawie sześć razy szybszy. Podklasowanie strnie jest wymagane i prawdopodobnie było złym pomysłem, ale ma pewne udogodnienia.

Znaki sortowania są duplikowane, aby wymusić sortowanie według wielkości liter, i zamieniane wielkością liter, aby wymusić sortowanie najpierw małych liter; jest to typowa definicja „rodzaju naturalnego”. Nie mogłem zdecydować o rodzaju grupowania; niektórzy mogą preferować następujące, co również przynosi znaczące korzyści w zakresie wydajności:

d = lambda s: s.lower()+s.swapcase()

Jeśli są używane, operatory porównania są ustawione na wartość, objectwięc nie będą ignorowane przezfunctools.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

Naturalne sortowanie jest zarówno dość skomplikowane, jak i niejasno zdefiniowane jako problem. Nie zapomnij uruchomić unicodedata.normalize(...)wcześniej, i rozważyć użycie str.casefold()zamiast str.lower(). Prawdopodobnie istnieją subtelne problemy z kodowaniem, których nie rozważałem. Dlatego wstępnie polecam bibliotekę natsort . Rzuciłem okiem na repozytorium github; utrzymanie kodu było gwiezdne.

Wszystkie algorytmy, które widziałem, zależą od sztuczek, takich jak powielanie i obniżanie znaków oraz zamiana wielkości liter. Chociaż podwaja to czas działania, alternatywa wymagałaby całkowitego naturalnego uporządkowania zestawu znaków wejściowych. Nie sądzę, że jest to część specyfikacji Unicode, a ponieważ jest o wiele więcej cyfr Unicode [0-9], tworzenie takiego sortowania byłoby równie zniechęcające. Jeśli chcesz porównań zależnych od ustawień regionalnych, przygotuj ciągi znaków, korzystając z locale.strxfrminstrukcji sortowania w Pythonie .

użytkownik19087
źródło
1

Pozwól mi przedstawić własne podejście do tej potrzeby:

from typing import Tuple, Union, Optional, Generator


StrOrInt = Union[str, int]


# On Python 3.6, string concatenation is REALLY fast
# Tested myself, and this fella also tested:
# https://blog.ganssle.io/articles/2019/11/string-concat.html
def griter(s: str) -> Generator[StrOrInt, None, None]:
    last_was_digit: Optional[bool] = None
    cluster: str = ""
    for c in s:
        if last_was_digit is None:
            last_was_digit = c.isdigit()
            cluster += c
            continue
        if c.isdigit() != last_was_digit:
            if last_was_digit:
                yield int(cluster)
            else:
                yield cluster
            last_was_digit = c.isdigit()
            cluster = ""
        cluster += c
    if last_was_digit:
        yield int(cluster)
    else:
        yield cluster
    return


def grouper(s: str) -> Tuple[StrOrInt, ...]:
    return tuple(griter(s))

Teraz, jeśli mamy taką listę:

filelist = [
    'File3', 'File007', 'File3a', 'File10', 'File11', 'File1', 'File4', 'File5',
    'File9', 'File8', 'File8b1', 'File8b2', 'File8b11', 'File6'
]

Możemy po prostu użyć key=kwarga, aby zrobić naturalny sposób:

>>> sorted(filelist, key=grouper)
['File1', 'File3', 'File3a', 'File4', 'File5', 'File6', 'File007', 'File8', 
'File8b1', 'File8b2', 'File8b11', 'File9', 'File10', 'File11']

Wadą tego rozwiązania jest oczywiście, podobnie jak teraz, funkcja sortująca wielkie litery przed małymi.

Wdrożenie czytelnika bez znaczenia dla sprawy pozostawię czytelnikowi :-)

pepoluan
źródło
0

Sugeruję po prostu użyć keyargumentu słowa kluczowego w sortedcelu uzyskania pożądanej listy
Na przykład:

to_order= [e2,E1,e5,E4,e3]
ordered= sorted(to_order, key= lambda x: x.lower())
    # ordered should be [E1,e2,e3,E4,e5]
Johny Vaknin
źródło
1
to nie obsługuje cyfr. a_51byłoby później a500, chociaż 500> 51
skjerns
To prawda, moja odpowiedź po prostu pasuje do podanego przykładu Elm11 i elm1. Pominięto prośbę o sortowanie naturalne, a zaznaczona odpowiedź jest prawdopodobnie najlepsza tutaj :)
Johny Vaknin
0

Po odpowiedzi @ Mark Byers, oto adaptacja, która akceptuje keyparametr i jest bardziej zgodna z PEP8.

def natsorted(seq, key=None):
    def convert(text):
        return int(text) if text.isdigit() else text

    def alphanum(obj):
        if key is not None:
            return [convert(c) for c in re.split(r'([0-9]+)', key(obj))]
        return [convert(c) for c in re.split(r'([0-9]+)', obj)]

    return sorted(seq, key=alphanum)

Zrobiłem też Gist

edouardtheron
źródło
(-1) ta odpowiedź nie wnosi nic nowego w porównaniu do Mark'a (każdy linijka może PEP8-ify jakiś kod). A może keyparametr? Ale jest to również zilustrowane w odpowiedzi @ beauburrier
Ciprian Tomoiagă
0

Poprawa poprawy Claudiu w odpowiedzi Marka Byera ;-)

import re

def natural_sort_key(s, _re=re.compile(r'(\d+)')):
    return [int(t) if i & 1 else t.lower() for i, t in enumerate(_re.split(s))]

...
my_naturally_sorted_list = sorted(my_list, key=natural_sort_key)

BTW, może nie wszyscy pamiętają, że wartości domyślne argumentów funkcji są oceniane w defczasie

Walter Tross
źródło
-1
a = ['H1', 'H100', 'H10', 'H3', 'H2', 'H6', 'H11', 'H50', 'H5', 'H99', 'H8']
b = ''
c = []

def bubble(bad_list):#bubble sort method
        length = len(bad_list) - 1
        sorted = False

        while not sorted:
                sorted = True
                for i in range(length):
                        if bad_list[i] > bad_list[i+1]:
                                sorted = False
                                bad_list[i], bad_list[i+1] = bad_list[i+1], bad_list[i] #sort the integer list 
                                a[i], a[i+1] = a[i+1], a[i] #sort the main list based on the integer list index value

for a_string in a: #extract the number in the string character by character
        for letter in a_string:
                if letter.isdigit():
                        #print letter
                        b += letter
        c.append(b)
        b = ''

print 'Before sorting....'
print a
c = map(int, c) #converting string list into number list
print c
bubble(c)

print 'After sorting....'
print c
print a

Podziękowania :

Bubble Sort Homework

Jak czytać ciąg po jednej literze w pythonie

Varadaraju G.
źródło
-2
>>> import re
>>> sorted(lst, key=lambda x: int(re.findall(r'\d+$', x)[0]))
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
SilentGhost
źródło
4
Twoje wdrożenie rozwiązuje tylko problem z liczbami. Implementacja kończy się niepowodzeniem, jeśli ciągi nie zawierają liczb. Wypróbuj na przykład w [cichym, duchu] (indeks listy poza zakresem).
snakile
2
@ snaklie: twoje pytanie nie zawiera przyzwoitego przykładu. Nie wyjaśniłeś, co próbujesz zrobić, ani nie zaktualizowałeś swojego pytania o te nowe informacje. Nie opublikowałeś niczego, co próbowałeś, więc proszę, nie lekceważ mojej próby telepatii.
SilentGhost 29.01.11
5
@SilentGhost: Po pierwsze, dałem ci głos, ponieważ uważam, że twoja odpowiedź jest przydatna (nawet jeśli to nie rozwiązuje mojego problemu). Po drugie, nie mogę opisać wszystkich możliwych przypadków przykładami. Myślę, że podałem całkiem jasną definicję rodzajowi naturalnemu. Nie sądzę, że dobrym pomysłem jest dawanie złożonego przykładu lub długiej definicji tak prostej koncepcji. Możesz edytować moje pytanie, jeśli możesz wymyślić lepsze sformułowanie problemu.
snakile 29.01.11
1
@SilentGhost: Chciałbym postępować z takimi ciągami w taki sam sposób, jak Windows radzi sobie z takimi nazwami plików, gdy sortuje pliki według nazw (ignoruj ​​przypadki itp.). Wydaje mi się to jasne, ale wszystko, co mówię, wydaje mi się jasne, więc nie oceniam, czy to jasne, czy nie.
snakile
1
@ snakile, nigdzie nie zbliżyłeś się do zdefiniowania naturalnego wyszukiwania. Byłoby to dość trudne i wymagałoby wielu szczegółów. Jeśli chcesz porządek sortowania używany przez Eksploratora Windows, czy wiesz, że istnieje proste wywołanie interfejsu API, które to zapewnia?
David Heffernan