Existe uma função interna para classificação natural de cadeias?


281

Usando o Python 3.x, tenho uma lista de cadeias de caracteres para as quais gostaria de executar uma classificação alfabética natural.

Tipo natural: a ordem pela qual os arquivos no Windows são classificados.

Por exemplo, a lista a seguir é classificada naturalmente (o que eu quero):

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

E aqui está a versão "classificada" da lista acima (o que eu tenho):

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

Estou procurando uma função de classificação que se comporte como a primeira.


13
A definição de uma classificação natural não é "a ordem em que o Windows classifica os arquivos".
Glenn Maynard


Todas as respostas neste site produzirão resultados incorretos se você desejar a classificação 'semelhante ao Windows Explorer' em vários casos, por exemplo !1, 1, !a, a. A única maneira de classificar como o Windows parece ser usar a StrCmpLogicalW própria função do Windows , pois ninguém parece ter reimplementado essa função corretamente (a fonte seria apreciada). Solução: stackoverflow.com/a/48030307/2441026
user136036

Respostas:


235

Existe uma biblioteca de terceiros para isso no PyPI chamada natsort (divulgação completa, eu sou o autor do pacote). Para o seu caso, você pode fazer o seguinte:

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

Você deve observar que natsortusa um algoritmo geral, portanto ele deve funcionar para praticamente qualquer entrada que você lançar nele. Se você quiser mais detalhes sobre por que você pode escolher uma biblioteca para fazer isso em vez de rolar a sua própria função, consulte a natsortdocumentação é Como Funciona página, em particular os casos especiais em toda parte! seção.


Se você precisar de uma chave de classificação em vez de uma função de classificação, use uma das fórmulas abaixo.

>>> 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
Também acho interessante que o natsort também esteja correto quando o número não estiver no final: como costuma ser o caso dos nomes de arquivos. Sinta-se à vontade para incluir o seguinte exemplo: pastebin.com/9cwCLdEK
Martin Thoma

1
Natsort é uma ótima biblioteca, deve ser adicionada à biblioteca padrão do python! :-)
Mitch McMabers

natsorttambém 'naturalmente' lida com o caso de vários números separados nas strings. Coisas boas!
FlorianH 13/04

182

Tente o seguinte:

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)

Resultado:

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

Código adaptado daqui: Classificação para seres humanos: ordem de classificação natural .


2
por que você usa em return sorted(l, key)vez de l.sort(key)? É para qualquer ganho de desempenho ou apenas para ser mais pitônico?
jperelli

12
@ jperelli Acho que a escada mudaria a lista original no chamador. Mas provavelmente o chamador deseja outra cópia superficial da lista.
Huggie

3
Apenas para o registro, isso não pode lidar com todas as entradas: as divisões str / int devem se alinhar; caso contrário, você criará comparações como ["foo", 0] <[0, "foo"] para a entrada ["foo0 "," 0foo "], que gera um TypeError.
precisa saber é o seguinte

4
@ user19087: De fato, funciona, porque re.split('([0-9]+)', '0foo')retorna ['', '0', 'foo']. Por isso, as strings sempre estarão em índices pares e números inteiros em índices ímpares na matriz.
Florian Kusche 30/10

Para quem quer saber sobre desempenho, isso é notavelmente mais lento que o tipo nativo do python. ou seja, 25 -50x mais lento. E se você deseja sempre classificar [elm1, elm2, Elm2, elm2] como [elm1, Elm2, elm2, elm2] de maneira confiável (limites antes da parte inferior), basta chamar natural_sort (classificado (lst)). Mais ineficiente, mas muito fácil de obter uma classificação repetível. Compile o regex para uma aceleração de ~ 50%. como visto na resposta de Claudiu.
Charlie Haley

99

Aqui está uma versão muito mais pitônica da resposta de 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)]    

Agora, esta função pode ser usada como uma chave em qualquer função que usa-lo, como list.sort, sorted,max , etc.

Como um lambda:

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

9
O módulo re compila e armazena em cache regexes automagicamente, portanto, não há necessidade de pré
compilar #

1
@ wim: armazena em cache os últimos usos do X, por isso é tecnicamente possível usar as expressões regulares X + 5 e, em seguida, fazer uma classificação natural repetidas vezes, e nesse ponto isso não seria armazenado em cache. mas provavelmente insignificante a longo prazo
Claudiu

Eu não fiz isso, mas talvez o motivo tenha sido que ele não pode lidar com tuplas, como uma espécie de python comum.
The Unfun Cat

1
Os usos X mencionados por @Claudiu parecem ser 100 no Python 2.7 e 512 no Python 3.4. Observe também que, quando o limite é atingido, o cache é completamente limpo (portanto, não é apenas o mais antigo que é descartado).
Zitrax

@Zitrax Por que / como faz sentido limpar o cache completamente?
1927 Joschua

19

Eu escrevi uma função baseada em http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html que adiciona a capacidade de ainda passar seu próprio parâmetro 'key'. Eu preciso disso para executar um tipo natural de listas que contêm objetos mais complexos (não apenas seqüências de caracteres).

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)

Por exemplo:

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

uma maneira mais simples de fazer isso seria definir natural_sort_keye, em seguida, ao classificar uma lista você poderia fazer cadeia de suas chaves, por exemplo:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu

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

Vamos analisar os dados. A capacidade de dígitos de todos os elementos é 2. E há 3 letras na parte literal comum 'elm'.

Portanto, o comprimento máximo do elemento é 5. Podemos aumentar esse valor para garantir (por exemplo, para 8).

Tendo isso em mente, temos uma solução de uma linha:

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

sem expressões regulares e bibliotecas externas!

print(data)

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

Explicação:

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

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

1
Isso não trata dados dinâmicos / desconhecidos. Ele também classifica de maneira diferente de outras soluções para dados que possuem números dentro dos dados opostos no final. * Isso não é necessariamente indesejável, mas acho bom ressaltar.
JerodG

1
Se você precisar manipular dados dinâmicos de comprimento, poderá width = max(data, key=len)calcular o que deseja submarino para os 8'{0:0>{width}}'.format(x, width=width)
itens

1
Apenas fazendo um teste cronometrado em comparação com todos os outros deste fórum, esta solução é de longe a mais rápida e eficiente para o tipo de dados que o @snakile está tentando processar
SR Colledge

13

Dado:

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

Semelhante à solução da SergO, um liner sem bibliotecas externas seria :

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

ou

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

Explicação:

Esta solução usa os principais recursos de classificação para definir uma função que será empregada para a classificação. Como sabemos que toda entrada de dados é precedida por 'elm', a função de classificação converte em número inteiro a parte da sequência após o terceiro caractere (ou seja, int (x [3:])). Se a parte numérica dos dados estiver em um local diferente, essa parte da função precisará ser alterada.

Felicidades


6
E agora, para algo mais * elegante (pitônico) - basta um toque

Existem muitas implementações por aí e, embora algumas tenham chegado perto, nenhuma capturou a elegância que o python moderno oferece.

  • Testado usando python (3.5.1)
  • Incluiu uma lista adicional para demonstrar que funciona quando os números estão no meio da sequência
  • Não testei, no entanto, estou assumindo que, se sua lista fosse considerável, seria mais eficiente compilar o regex antecipadamente
    • Tenho certeza de que alguém vai me corrigir se essa é uma suposição errônea

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)])
Código completo
#!/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']

Cuidado ao usar

  • from os.path import split
    • você precisará diferenciar as importações

Inspiração de


6

Valor deste post

Meu objetivo é oferecer uma solução não regex que possa ser aplicada em geral.
Vou criar três funções:

  1. find_first_digitque peguei emprestado da @AnuragUniyal . Ele encontrará a posição do primeiro dígito ou não dígito em uma sequência.
  2. split_digitsque é um gerador que separa uma string em pedaços de dígitos e não dígitos. Também yieldnúmeros inteiros quando é um dígito.
  3. natural_keyapenas envolve split_digitsem um tuple. Isso é o que usamos como uma chave para sorted, max, min.

Funções

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

Podemos ver que é geral, pois podemos ter blocos de vários dígitos:

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

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

Ou deixe como diferencia maiúsculas de minúsculas:

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

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

Podemos ver que ele classifica a lista do OP na ordem apropriada

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

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

Mas ele também pode lidar com listas mais complicadas:

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

Meu equivalente regex seria

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
Muito obrigado! Quero acrescentar, no entanto, que se você tiver "12345_A" e "12345_A2", o último será classificado antes do primeiro. Pelo menos não é assim que o Windows faz. Ainda funciona para o problema acima!
morph3us 16/01

4

Uma opção é transformar a string em uma tupla e substituir dígitos usando o formulário expandido http://wiki.answers.com/Q/What_does_expanded_form_mean

assim a90 se tornaria ("a", 90,0) e a1 se tornaria ("a", 1)

abaixo está um código de exemplo (que não é muito eficiente devido à maneira como remove os zeros iniciais dos números)

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)

resultado:

('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
Infelizmente, esta solução funciona apenas para o Python 2.X. Para Python 3, ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)retornaráTypeError: unorderable types: int() < str()
SethMMorton 3/17/17

@SethMMorgon é certo, este código facilmente breaks em Python 3. A alternativa natural parece natsort, pypi.org/project/natsort
FlorianH

3

Com base nas respostas aqui, escrevi uma natural_sortedfunção que se comporta como a função interna 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)

O código-fonte também está disponível no repositório de trechos do GitHub: https://github.com/bdrung/snippets/blob/master/natural_sorted.py


2

As respostas acima são boas para o exemplo específico que foi mostrado, mas faltam vários casos úteis para a questão mais geral de natureza natural. Acabei de entender um pouco desses casos, então criei uma solução mais 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

O código de teste e vários links (dentro e fora do StackOverflow) estão aqui: http://productarchitect.com/code/better-natural-sort.py

Feedback bem-vindo. Isso não pretende ser uma solução definitiva; apenas um passo à frente.


No script de teste ao qual você vincula natsortede humansortedfalha porque foram usadas incorretamente ... você tentou passar natsortedcomo uma chave, mas na verdade é a própria função de classificação. Você deveria ter tentado natsort_keygen().
SethMMorton

2

Muito provavelmente functools.cmp_to_key()está intimamente ligado à implementação subjacente da classificação do python. Além disso, o parâmetro cmp é legado. A maneira moderna é transformar os itens de entrada em objetos que suportam as operações de comparação avançada desejadas.

No CPython 2.x, objetos de tipos diferentes podem ser pedidos, mesmo que os respectivos operadores de comparação avançada não tenham sido implementados. No CPython 3.x, objetos de diferentes tipos devem oferecer suporte explícito à comparação. Veja Como o Python compara string e int? que liga à documentação oficial . A maioria das respostas depende dessa ordem implícita. Mudar para o Python 3.x exigirá um novo tipo para implementar e unificar comparações entre números e seqüências de caracteres.

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

Existem três abordagens diferentes. O primeiro usa classes aninhadas para tirar proveito do Iterablealgoritmo de comparação do Python . O segundo desenrola esse aninhamento em uma única classe. O terceiro antecede a subclasse strpara focar no desempenho. Todos são cronometrados; o segundo é duas vezes mais rápido, enquanto o terceiro quase seis vezes mais rápido. A subclassificação strnão é necessária e, provavelmente, foi uma má ideia, mas vem com certas conveniências.

Os caracteres de classificação são duplicados para forçar a ordenação por caso e trocados entre maiúsculas e minúsculas para forçar a letra minúscula a classificar primeiro; esta é a definição típica de "classificação natural". Não pude decidir sobre o tipo de agrupamento; alguns podem preferir o seguinte, o que também traz benefícios significativos de desempenho:

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

Onde utilizados, os operadores de comparação são configurados para objectnão serem ignoradosfunctools.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

A classificação natural é bastante complicada e vagamente definida como um problema. Não se esqueça de executar com unicodedata.normalize(...)antecedência e considere o uso str.casefold()e não str.lower(). Provavelmente existem problemas sutis de codificação que não considerei. Então, eu recomendo a biblioteca natsort . Dei uma rápida olhada no repositório do github; a manutenção do código foi estelar.

Todos os algoritmos que eu vi dependem de truques, como duplicar e diminuir caracteres e trocar de maiúsculas e minúsculas. Embora isso duplique o tempo de execução, uma alternativa exigiria uma ordem natural total no conjunto de caracteres de entrada. Eu não acho que isso faça parte da especificação unicode e, como existem muitos mais dígitos unicode do que [0-9], criar essa classificação seria igualmente assustador. Se você deseja comparações com reconhecimento de localidade, prepare suas seqüências de locale.strxfrmacordo com o HOWTO de Classificação do Python .


1

Deixe-me enviar minha opinião sobre essa necessidade:

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

Agora, se tivermos a lista como esta:

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

Podemos simplesmente usar o key=kwarg para fazer um tipo natural:

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

A desvantagem aqui é, obviamente, como é agora, a função classificará letras maiúsculas antes de letras minúsculas.

Vou deixar a implementação de uma garoupa que não diferencia maiúsculas de minúsculas para o leitor :-)


0

Sugiro que você simplesmente use o keyargumento de palavra - chave de sortedpara obter a lista desejada.
Por exemplo:

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
isso não manipula dígitos. a_51seria depois a500, embora 500> 51
skjerns 28/02

É verdade que minha resposta simplesmente corresponde ao exemplo dado de Elm11 e elm1. Perdeu o pedido de classificação natural especificamente e a resposta marcada é provavelmente a melhor aqui :)
Johny Vaknin

0

Após a resposta do @Mark Byers, aqui está uma adaptação que aceita o keyparâmetro e é mais compatível com 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)

Eu também fiz um Gist


(-1) esta resposta não traz nada de novo em comparação com o de Mark (qualquer ponteiro pode PEP8-ify algum código). Ou talvez o keyparâmetro? Mas isso também é exemplificado na resposta de @ beauburrier
Ciprian Tomoiagă

0

Uma melhoria na melhoria de Claudiu na resposta de 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)

BTW, talvez nem todos se lembrem de que os padrões dos argumentos das funções são avaliados no defmomento


-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

Agradecimentos :

Bubble Sort Homework

Como ler uma string de uma letra por vez em 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
Sua implementação resolve apenas o problema dos números. A implementação falhará se as cadeias não tiverem números. Experimente em ['silencioso', 'fantasma'], por exemplo (índice da lista fora do intervalo).
snakile

2
@snaklie: sua pergunta não fornece um exemplo de caso decente. Você não explicou o que está tentando fazer e nem atualizou sua pergunta com essas novas informações. Você não postou nada do que tentou, por isso não menospreze minha tentativa de telepatia.
SilentGhost

5
@ SilentGhost: Primeiro, eu te dei uma votação positiva, porque acho que sua resposta é útil (mesmo que não resolva meu problema). Segundo, não posso cobrir todos os casos possíveis com exemplos. Acho que dei uma definição bastante clara ao tipo natural. Não acho que seja uma boa idéia dar um exemplo complexo ou uma longa definição a um conceito tão simples. Você pode editar minha pergunta se puder pensar em uma melhor formulação para o problema.
snakile

1
@ SilentGhost: eu gostaria de lidar com essas seqüências da mesma maneira que o Windows lida com esses nomes de arquivos quando classifica os arquivos por nome (ignore casos, etc.). Parece claro para mim, mas tudo o que digo parece claro para mim, então não devo julgar se está claro ou não.
snakile

1
@snakile, você chegou nem perto de definir a pesquisa natural. Isso seria bastante difícil de fazer e exigiria muitos detalhes. Se você deseja a ordem de classificação usada pelo Windows Explorer, você sabe que existe uma chamada API simples que fornece isso?
David Heffernan
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.