Esiste una funzione integrata per l'ordinamento naturale delle stringhe?


281

Usando Python 3.x, ho un elenco di stringhe per le quali vorrei eseguire un ordinamento alfabetico naturale.

Ordinamento naturale: l'ordine in base al quale vengono ordinati i file in Windows.

Ad esempio, il seguente elenco è naturalmente ordinato (quello che voglio):

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

Ed ecco la versione "ordinata" dell'elenco sopra (quello che ho):

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

Sto cercando una funzione di ordinamento che si comporti come la prima.


13
La definizione di un ordinamento naturale non è "l'ordine in cui Windows ordina i file".
Glenn Maynard,


Tutte le risposte su questo sito produrranno risultati errati se si desidera l' ordinamento "simile a Windows Explorer" in diversi casi, ad esempio l'ordinamento !1, 1, !a, a. L'unico modo per ottenere l'ordinamento come Windows sembra essere quello di utilizzare la StrCmpLogicalW stessa funzione di Windows , poiché nessuno sembra aver reimplementato correttamente questa funzione (la fonte sarebbe apprezzata). Soluzione: stackoverflow.com/a/48030307/2441026
user136036

Risposte:


235

Su PyPI esiste una libreria di terze parti chiamata natsort (informativa completa, sono l'autore del pacchetto). Nel tuo caso, puoi effettuare una delle seguenti operazioni:

>>> 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']

Dovresti notare che natsortusa un algoritmo generale, quindi dovrebbe funzionare praticamente per qualsiasi input che gli passi. Se vuoi maggiori dettagli sul motivo per cui potresti scegliere una libreria per farlo piuttosto che implementare la tua funzione, controlla la pagina How It Works della natsortdocumentazione , in particolare Casi speciali ovunque! sezione.


Se è necessaria una chiave di ordinamento anziché una funzione di ordinamento, utilizzare una delle seguenti formule.

>>> 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']

5
Penso anche che sia abbastanza interessante che anche natsort ordini correttamente quando il numero non è alla fine: come spesso accade per i nomi dei file. Sentiti libero di includere il seguente esempio: pastebin.com/9cwCLdEK
Martin Thoma,

1
Natsort è una grande libreria, dovrebbe essere aggiunta alla libreria standard di Python! :-)
Mitch McMabers,

natsortinoltre 'naturalmente' gestisce il caso di più numeri separati nelle stringhe. Roba fantastica!
FlorianH

182

Prova questo:

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)

Produzione:

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

Codice adattato da qui: ordinamento per esseri umani: ordinamento naturale .


2
perché usi return sorted(l, key)invece di l.sort(key)? È per qualche miglioramento delle prestazioni o semplicemente per essere più pitonico?
jperelli,

12
@jperelli Penso che la scala cambierebbe l'elenco originale nel chiamante. Ma molto probabilmente il chiamante vuole un'altra copia superficiale dell'elenco.
huggie,

3
Solo per la cronaca, questo non può gestire tutti gli input: gli spl / str devono essere allineati, altrimenti creerai confronti come ["pippo", 0] <[0, "pippo"] per l'input ["pippo0 "," 0foo "], che genera un TypeError.
user19087

4
@ user19087: In effetti funziona, perché re.split('([0-9]+)', '0foo')restituisce ['', '0', 'foo']. Per questo motivo, le stringhe saranno sempre su indici e interi pari su indici dispari nell'array.
Florian Kusche,

Per chiunque si chieda delle prestazioni, questo è notevolmente più lento dell'ordinamento nativo di Python. cioè 25-50 volte più lento. E se vuoi ordinare sempre [elm1, elm2, Elm2, elm2] come [elm1, Elm2, elm2, elm2] in modo affidabile (maiuscolo prima), puoi semplicemente chiamare natural_sort (ordinato (lst)). Più inefficiente, ma molto facile da ottenere un ordinamento ripetibile. Compila il regex per uno speedup del 50% circa. come si vede nella risposta di Claudiu.
Charlie Haley,

100

Ecco una versione molto più pitonica della risposta di Mark Byer:

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

Ora, questa funzione può essere utilizzata come chiave in qualsiasi funzione che lo utilizza, come list.sort, sorted, max, etc.

Come una lambda:

lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]

9
re module compila e memorizza nella cache le regex in modo automatico, quindi non è necessario precompilare
wim

1
@wim: memorizza nella cache gli ultimi utilizzi X, quindi è tecnicamente possibile usare le regex X + 5 e fare un ordinamento naturale più e più volte, a quel punto questo non verrebbe memorizzato nella cache. ma probabilmente trascurabile a lungo termine
Claudiu il

Non l'ho fatto, ma forse il motivo era che non è in grado di gestire le tuple, come un normale ordinamento in pitone.
The Unfun Cat,

1
Gli usi X citati da @Claudiu sembrano essere 100 su Python 2.7 e 512 su Python 3.4. E nota anche che quando viene raggiunto il limite la cache viene completamente cancellata (quindi non è solo quella più vecchia che viene eliminata).
Zitrax,

@Zitrax Perché / come ha senso svuotare completamente la cache?
Joschua,

19

Ho scritto una funzione basata su http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html che aggiunge la possibilità di passare ancora il tuo parametro 'chiave'. Ne ho bisogno per eseguire una sorta di elenco naturale che contenga oggetti più complessi (non solo stringhe).

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)

Per esempio:

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'}]

un modo più semplice per farlo sarebbe quello di definire natural_sort_key, e quindi quando si ordina un elenco è possibile fare una catena di chiavi, ad esempio:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu

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

Analizziamo i dati. La capacità delle cifre di tutti gli elementi è 2. E ci sono 3 lettere nella parte letterale comune'elm' .

Quindi, la lunghezza massima dell'elemento è 5. Possiamo aumentare questo valore per assicurarci (ad esempio, a 8).

Tenendo presente ciò, abbiamo una soluzione a una riga:

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

senza espressioni regolari e librerie esterne!

print(data)

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

Spiegazione:

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

>>>
0000elm0
0000elm1
0000elm2
0000elm9
000elm10
000elm11
000elm13

1
Questo non gestisce i dati di lunghezza dinamica / sconosciuta. Inoltre, ordina in modo diverso rispetto ad altre soluzioni per i dati che hanno numeri all'interno dei dati opposti alla fine. * Questo non è necessariamente indesiderabile, ma penso che sia bene sottolineare.
JerodG,

1
Se è necessario gestire i dati di lunghezza dinamica, è possibile utilizzare width = max(data, key=len)per calcolare gli elementi da sottoporre per quanto 8sopra e quindi immetterli nella stringa di formato con'{0:0>{width}}'.format(x, width=width)
roganartu,

1
Solo facendo un test a tempo rispetto a tutti gli altri su questo forum, questa soluzione è di gran lunga la più veloce ed efficiente per il tipo di dati che @snakile sta cercando di elaborare
SR Colledge

13

Dato:

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

Simile alla soluzione di SergO, un 1-liner senza librerie esterne sarebbe :

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

o

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

Spiegazione:

Questa soluzione utilizza la funzionalità chiave di ordinamento per definire una funzione che verrà utilizzata per l'ordinamento. Poiché sappiamo che ogni voce di dati è preceduta da 'elm', la funzione di ordinamento converte in numero intero la parte della stringa dopo il 3 ° carattere (cioè int (x [3:])). Se la parte numerica dei dati si trova in una posizione diversa, questa parte della funzione dovrebbe cambiare.

Saluti


6
E ora qualcosa di più * elegante (pitonico): solo un tocco

Ci sono molte implementazioni là fuori e mentre alcune si sono avvicinate, nessuna ha catturato l'eleganza che offre il moderno pitone.

  • Testato usando Python (3.5.1)
  • Incluso un elenco aggiuntivo per dimostrare che funziona quando i numeri sono al centro della stringa
  • Non ho testato, tuttavia, presumo che se la tua lista fosse considerevole sarebbe più efficiente compilare la regex in anticipo
    • Sono sicuro che qualcuno mi correggerà se questa è un'ipotesi errata

Quicky
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)])
Full-codice
#!/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']

Attenzione durante l'utilizzo

  • from os.path import split
    • dovrai differenziare le importazioni

Ispirazione da


6

Valore di questo post

Il mio punto è offrire una soluzione non regex che possa essere applicata in generale.
Creerò tre funzioni:

  1. find_first_digitche ho preso in prestito da @AnuragUniyal . Troverà la posizione della prima cifra o non cifra in una stringa.
  2. split_digitsche è un generatore che separa una stringa in blocchi di cifre e non di cifre. Sarà anche yieldnumeri interi quando è una cifra.
  3. natural_keysi avvolge split_digitsin a tuple. Questo è quello che usiamo come una chiave per sorted, max, min.

funzioni

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

Possiamo vedere che è generale che possiamo avere blocchi di più cifre:

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

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

O lascia come maiuscole / minuscole:

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

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

Possiamo vedere che ordina l'elenco dei PO nell'ordine appropriato

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

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

Ma può gestire anche elenchi più complicati:

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']

Il mio equivalente regex sarebbe

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

1
Molte grazie! Voglio aggiungere, tuttavia, che se si dispone di "12345_A" e "12345_A2", quest'ultimo verrà ordinato prima del primo. Almeno non è così che fa Windows. Funziona ancora per il problema sopra, però!
morph3us

4

Un'opzione è trasformare la stringa in una tupla e sostituire le cifre usando il modulo espanso http://wiki.answers.com/Q/What_does_expanded_form_mean

in questo modo a90 diventerebbe ("a", 90,0) e a1 diventerebbe ("a", 1)

di seguito è riportato un codice di esempio (che non è molto efficiente a causa del modo in cui rimuove gli 0 iniziali dai numeri)

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)

produzione:

('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']

1
Sfortunatamente, questa soluzione funziona solo per Python 2.X. Per Python 3, ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)torneràTypeError: unorderable types: int() < str()
SethMMorton

@SethMMorgon ha ragione, questo codice si rompe facilmente in Python 3. Sembrerebbe un'alternativa naturale natsort, pypi.org/project/natsort
FlorianH

3

Sulla base delle risposte qui, ho scritto una natural_sortedfunzione che si comporta come la funzione integrata sorted:

# Copyright (C) 2018, Benjamin Drung <bdrung@posteo.de>
#
# 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)

Il codice sorgente è disponibile anche nel mio repository di snippet GitHub: https://github.com/bdrung/snippets/blob/master/natural_sorted.py


2

Le risposte di cui sopra sono utili per l' esempio specifico mostrato, ma mancano diversi casi utili per la domanda più generale di tipo naturale. Ho appena ricevuto un morso da uno di questi casi, quindi ho creato una soluzione più completa:

def natural_sort_key(string_or_number):
    """
    by Scott S. Lawton <scott@ProductArchitect.com> 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

Codice di test e diversi collegamenti (dentro e fuori StackOverflow) sono qui: http://productarchitect.com/code/better-natural-sort.py

Feedback di benvenuto. Non è pensato per essere una soluzione definitiva; solo un passo avanti.


Nel tuo script di test a cui ti colleghi natsortede humansortedfallisci perché sono stati usati in modo errato ... hai provato a passare natsortedcome chiave ma in realtà è la stessa funzione di ordinamento. Avresti dovuto provare natsort_keygen().
SethMMorton,

2

Molto probabilmente functools.cmp_to_key()è strettamente legato all'implementazione sottostante dell'ordinamento di Python. Inoltre, il parametro cmp è legacy. Il modo moderno è di trasformare gli elementi di input in oggetti che supportano le ricche operazioni di confronto desiderate.

In CPython 2.x, è possibile ordinare oggetti di tipi diversi anche se i rispettivi operatori di confronto avanzato non sono stati implementati. In CPython 3.x, gli oggetti di tipi diversi devono supportare esplicitamente il confronto. Vedi In che modo Python confronta string e int? che collega alla documentazione ufficiale . La maggior parte delle risposte dipende da questo ordinamento implicito. Il passaggio a Python 3.x richiederà un nuovo tipo per implementare e unificare i confronti tra numeri e stringhe.

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

Esistono tre approcci diversi. Il primo utilizza le classi nidificate per sfruttare l' Iterablealgoritmo di confronto di Python . Il secondo svolge questa nidificazione in un'unica classe. Il terzo rinuncia stralla sottoclasse per concentrarsi sulle prestazioni. Tutti sono cronometrati; il secondo è due volte più veloce mentre il terzo quasi sei volte più veloce. La sottoclasse strnon è richiesta, e probabilmente è stata una cattiva idea in primo luogo, ma presenta alcune comodità.

I caratteri di ordinamento vengono duplicati per forzare l'ordinamento per caso e scambiati per maiuscole per forzare prima la lettera minuscola a ordinare; questa è la definizione tipica di "ordinamento naturale". Non riuscivo a decidere il tipo di raggruppamento; alcuni potrebbero preferire quanto segue, il che porta anche significativi vantaggi in termini di prestazioni:

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

Laddove utilizzati, gli operatori di confronto sono impostati su quello di objectquindi non verranno ignoratifunctools.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

L'ordinamento naturale è alquanto complicato e vagamente definito come un problema. Non dimenticare di correre in unicodedata.normalize(...)anticipo, e considera l'uso str.casefold()piuttosto che str.lower(). Probabilmente ci sono problemi di codifica sottili che non ho considerato. Quindi raccomando provvisoriamente la libreria natsort . Ho dato una rapida occhiata al repository github; la manutenzione del codice è stata stellare.

Tutti gli algoritmi che ho visto dipendono da trucchi come la duplicazione e l'abbassamento dei caratteri e il caso di scambio. Mentre questo raddoppia il tempo di esecuzione, un'alternativa richiederebbe un ordinamento naturale totale sul set di caratteri di input. Non penso che questo faccia parte delle specifiche unicode, e poiché ci sono molte più cifre unicode rispetto a quelle [0-9], creare un tale ordinamento sarebbe altrettanto scoraggiante. Se desideri confronti a livello locale, prepara le tue stringhe con locale.strxfrml' ordinamento HOW TO di Python .


1

Consentitemi di presentare la mia opinione su questa esigenza:

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

Ora, se abbiamo l'elenco come tale:

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

Possiamo semplicemente usare il key=kwarg per fare un ordinamento naturale:

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

Lo svantaggio qui è ovviamente, come lo è ora, la funzione ordinerà le lettere maiuscole prima delle lettere minuscole.

Lascerò al lettore l'implementazione di una cernia che non distingue tra maiuscole e minuscole :-)


0

Ti suggerisco di utilizzare semplicemente l' keyargomento della parola chiave di sortedper ottenere l'elenco desiderato
Ad esempio:

to_order= [e2,E1,e5,E4,e3]
ordered= sorted(to_order, key= lambda x: x.lower())
    # ordered should be [E1,e2,e3,E4,e5]

1
questo non gestisce le cifre. a_51sarebbe dopo a500, anche se 500> 51
skjerns

È vero, la mia risposta corrisponde semplicemente all'esempio dato di Elm11 ed elm1. Ho perso la richiesta di ordinamento naturale in modo specifico e la risposta contrassegnata è probabilmente la migliore qui :)
Johny Vaknin,

0

Dopo la risposta @Mark Byers, ecco un adattamento che accetta il keyparametro ed è più conforme a 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)

Ho anche fatto un Gist


(-1) questa risposta non porta nulla di nuovo rispetto a quello di Mark (qualsiasi linter può PEP8-ify del codice). O forse il keyparametro? Ma questo è anche esemplificato nella risposta di @ beauburrier
Ciprian Tomoiagă

0

Un miglioramento sul miglioramento di Claudiu sulla risposta di Mark Byer ;-)

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)

A proposito, forse non tutti ricordano che le impostazioni predefinite degli argomenti delle funzioni vengono valutate alla defvolta


-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

Ringraziamenti :

Bubble Sort Homework

Come leggere una stringa una lettera alla volta in Python


-2
>>> import re
>>> sorted(lst, key=lambda x: int(re.findall(r'\d+$', x)[0]))
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']

4
L'implementazione risolve solo il problema dei numeri. L'implementazione non riesce se le stringhe non contengono numeri. Provalo su ['silent', 'ghost'] per esempio (elenco degli indici fuori portata).
snakile,

2
@snaklie: la tua domanda non riesce a fornire un caso decente di esempio. Non hai spiegato cosa stai cercando di fare, né hai aggiornato la tua domanda con queste nuove informazioni. Non hai pubblicato nulla che hai provato, quindi per favore non essere così sprezzante per il mio tentativo di telepatia.
SilentGhost,

5
@SilentGhost: in primo luogo, ti ho dato un voto perché penso che la tua risposta sia utile (anche se non risolve il mio problema). In secondo luogo, non posso coprire tutti i casi possibili con esempi. Penso di aver dato una definizione abbastanza chiara all'ordinamento naturale. Non credo sia una buona idea dare un esempio complesso o una lunga definizione a un concetto così semplice. Puoi modificare la mia domanda se riesci a pensare a una migliore formulazione del problema.
snakile,

1
@SilentGhost: Vorrei trattare tali stringhe nello stesso modo in cui Windows gestisce tali nomi di file quando ordina i file per nome (ignora i casi, ecc.). Mi sembra chiaro, ma tutto ciò che dico mi sembra chiaro, quindi non devo giudicare se sia chiaro o no.
snakile,

1
@snakile non sei venuto da nessuna parte vicino a definire la ricerca naturale. Sarebbe piuttosto difficile da fare e richiederebbe molti dettagli. Se vuoi il tipo di ordinamento utilizzato da Windows Explorer, sai che esiste una semplice chiamata API che fornisce questo?
David Heffernan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.