¿Existe una función incorporada para el ordenamiento natural de cadenas?

281

Usando Python 3.x, tengo una lista de cadenas para las cuales me gustaría realizar una ordenación alfabética natural.

Ordenación natural: el orden en que se ordenan los archivos en Windows.

Por ejemplo, la siguiente lista está naturalmente ordenada (lo que quiero):

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

Y aquí está la versión "ordenada" de la lista anterior (lo que tengo):

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

Estoy buscando una función de clasificación que se comporte como la primera.

snakile
fuente
13
La definición de una ordenación natural no es "el orden en que Windows clasifica los archivos".
Glenn Maynard
Todas las respuestas en este sitio producirán resultados incorrectos si desea una clasificación 'similar a la del Explorador de Windows' en varios casos, por ejemplo, la clasificación !1, 1, !a, a. La única forma de ordenar como Windows parece ser usar la StrCmpLogicalW función de Windows en sí, ya que nadie parece haber implementado esta función correctamente (se agradecería la fuente). Solución: stackoverflow.com/a/48030307/2441026
user136036

Respuestas:

235

Hay una biblioteca de terceros para esto en PyPI llamada natsort (divulgación completa, soy el autor del paquete). Para su caso, puede hacer lo siguiente:

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

Debe tener en cuenta que natsortutiliza un algoritmo general, por lo que debería funcionar para casi cualquier entrada que le arroje. Si desea obtener más detalles sobre por qué podría elegir una biblioteca para hacer esto en lugar de implementar su propia función, consulte la página Cómo funciona de la natsortdocumentación , en particular los Casos especiales en todas partes. sección.


Si necesita una clave de clasificación en lugar de una función de clasificación, use cualquiera de las fórmulas a continuación.

>>> 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
fuente
55
También creo que es bastante interesante que natsort también sea correcto cuando el número no está al final: como suele ser el caso de los nombres de archivo. Siéntase libre de incluir el siguiente ejemplo: pastebin.com/9cwCLdEK
Martin Thoma
1
¡Natsort es una gran biblioteca, debe agregarse a la biblioteca estándar de Python! :-)
Mitch McMabers
natsorttambién 'naturalmente' maneja el caso de múltiples números separados en las cadenas. ¡Buena cosa!
FlorianH
182

Prueba esto:

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)

Salida:

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

Código adaptado desde aquí: Clasificación para humanos: orden de clasificación natural .

Mark Byers
fuente
2
¿Por qué usas en return sorted(l, key)lugar de l.sort(key)? ¿Es para ganar rendimiento o simplemente para ser más pitónico?
jperelli
12
@jperelli Creo que la escalera cambiaría la lista original en la persona que llama. Pero lo más probable es que la persona que llama quiera otra copia superficial de la lista.
Huggie
3
Solo para el registro, esto no puede manejar todas las entradas: las divisiones str / int deben alinearse, de lo contrario creará comparaciones como ["foo", 0] <[0, "foo"] para la entrada ["foo0 "," 0foo "], que genera un error de tipo.
user19087
44
@ user19087: De hecho, funciona, porque re.split('([0-9]+)', '0foo')regresa ['', '0', 'foo']. Debido a eso, las cadenas siempre estarán en índices pares y enteros en índices impares en la matriz.
Florian Kusche
Para cualquiera que se pregunte sobre el rendimiento, esto es notablemente más lento que el tipo nativo de Python. es decir, 25 -50 veces más lento. Y si desea ordenar siempre [elm1, elm2, Elm2, elm2] como [elm1, Elm2, elm2, elm2] de forma confiable (mayúsculas antes de menor), simplemente puede llamar a natural_sort (sorted (lst)). Más ineficiente, pero muy fácil de obtener un tipo repetible. Compila la expresión regular para una aceleración de ~ 50%. como se ve en la respuesta de Claudiu.
Charlie Haley el
100

Aquí hay una versión mucho más pitónica de la respuesta 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)]    

Ahora bien, esta función se puede utilizar como una clave en cualquier función que lo utiliza, como list.sort, sorted, max, etc.

Como lambda:

lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]
Claudiu
fuente
9
re module compila y almacena en caché las expresiones regulares automáticamente, por lo que no hay necesidad de precompilar
wim
1
@wim: almacena en caché los últimos usos de X, por lo que es técnicamente posible usar expresiones regulares X + 5 y luego hacer una ordenación natural una y otra vez, momento en el que esto no se almacenaría en caché. pero probablemente insignificante a largo plazo
Claudiu
No lo hice, pero quizás la razón fue que no puede manejar tuplas, como una especie de pitón normal.
The Unfun Cat
1
Los usos X mencionados por @Claudiu parecen ser 100 en Python 2.7 y 512 en Python 3.4. Y también tenga en cuenta que cuando se alcanza el límite, el caché se borra por completo (por lo que no solo se descarta el más antiguo).
Zitrax
@Zitrax ¿Por qué / cómo tiene sentido borrar el caché por completo?
Joschua
19

Escribí una función basada en http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html que agrega la capacidad de pasar su propio parámetro 'clave'. Necesito esto para realizar un tipo natural de listas que contienen objetos más complejos (no solo cadenas).

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 ejemplo:

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
fuente
una forma más sencilla de hacerlo sería definir natural_sort_key, y luego, al ordenar una lista, podría encadenar sus llaves, por ejemplo:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu
17
data = ['elm13', 'elm9', 'elm0', 'elm1', 'Elm11', 'Elm2', 'elm10']

Analicemos los datos. La capacidad de dígitos de todos los elementos es 2. Y hay 3 letras en la parte literal común 'elm'.

Entonces, la longitud máxima del elemento es 5. Podemos aumentar este valor para asegurarnos (por ejemplo, a 8).

Teniendo esto en cuenta, tenemos una solución de una línea:

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

sin expresiones regulares y bibliotecas externas!

print(data)

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

Explicación:

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

>>>
0000elm0
0000elm1
0000elm2
0000elm9
000elm10
000elm11
000elm13
SergO
fuente
1
Esto no maneja datos de longitud dinámica / desconocida. También se clasifica de manera diferente a otras soluciones para datos que tienen números dentro de los datos opuestos al final. * Esto no es necesariamente indeseable, pero creo que es bueno señalarlo.
JerodG
1
Si necesita manejar datos de longitud dinámica, puede usar width = max(data, key=len)para calcular qué 8'{0:0>{width}}'.format(x, width=width)
sumar
1
Simplemente haciendo una prueba cronometrada en comparación con todos los demás en este foro, esta solución es, con mucho, la más rápida y eficiente para el tipo de datos que @snakile está tratando de procesar
SR Colledge
13

Dado:

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

Similar a la solución de SergO, un 1-liner sin bibliotecas externas sería :

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

o

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

Explicación:

Esta solución utiliza la característica clave de sort para definir una función que se empleará para la clasificación. Como sabemos que cada entrada de datos está precedida por 'elm', la función de clasificación se convierte en un número entero en la parte de la cadena después del 3er carácter (es decir, int (x [3:])). Si la parte numérica de los datos está en una ubicación diferente, entonces esta parte de la función tendría que cambiar.

Salud

Camilo
fuente
6
Y ahora por algo más * elegante (pitón) un toque

Hay muchas implementaciones por ahí, y aunque algunas se han acercado, ninguna captó la elegancia que ofrece la moderna Python.

  • Probado con python (3.5.1)
  • Se incluye una lista adicional para demostrar que funciona cuando los números están en la mitad de la cadena
  • Sin embargo, no probé, supongo que si su lista fuera considerable, sería más eficiente compilar la expresión regular de antemano
    • Estoy seguro de que alguien me corregirá si esto es una suposición 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']

Precaución al usar

  • from os.path import split
    • necesitarás diferenciar las importaciones

Inspiración de

JerodG
fuente
6

Valor de esta publicación

Mi punto es ofrecer una solución no regex que se pueda aplicar en general.
Crearé tres funciones:

  1. find_first_digitque tomé prestado de @AnuragUniyal . Encontrará la posición del primer dígito o no dígito en una cadena.
  2. split_digitsque es un generador que separa una cadena en fragmentos de dígitos y no dígitos. También será yieldun número entero cuando sea un dígito.
  3. natural_keysimplemente se envuelve split_digitsen un tuple. Esto es lo que usamos como una clave para sorted, max, min.

Las funciones

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 es general, ya que podemos tener fragmentos de varios dígitos:

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

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

O dejar como mayúsculas y minúsculas:

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

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

Podemos ver que ordena la lista de OP en el orden apropiado

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

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

Pero también puede manejar listas más 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']

Mi equivalente de expresiones regulares sería

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
fuente
1
¡Muchas gracias! Sin embargo, quiero agregar que si tiene "12345_A" y "12345_A2", este último se ordenará antes que el primero. Al menos no es así como lo hace Windows. Sin embargo, todavía funciona para el problema anterior.
morph3us
4

Una opción es convertir la cadena en una tupla y reemplazar los dígitos usando la forma expandida http://wiki.answers.com/Q/What_does_expanded_form_mean

de esa manera a90 se convertiría ("a", 90,0) y a1 se convertiría ("a", 1)

a continuación hay un código de muestra (que no es muy eficiente debido a la forma en que elimina los ceros a la izquierda de los 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)

salida:

('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
fuente
1
Desafortunadamente, esta solución solo funciona para Python 2.X. Para Python 3, ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)volveráTypeError: unorderable types: int() < str()
SethMMorton
@SethMMorgon tiene razón, este código se rompe fácilmente en Python 3. La alternativa natural parecería natsort, pypi.org/project/natsort
FlorianH
3

Basado en las respuestas aquí, escribí una natural_sortedfunción que se comporta como la función incorporada 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)

El código fuente también está disponible en mi repositorio de fragmentos de GitHub: https://github.com/bdrung/snippets/blob/master/natural_sorted.py

Benjamin Drung
fuente
2

Las respuestas anteriores son buenas para el ejemplo específico que se mostró, pero pierden varios casos útiles para la pregunta más general de tipo natural. Acabo de ser mordido por uno de esos casos, así que creé una solución más completa:

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

El código de prueba y varios enlaces (dentro y fuera de StackOverflow) están aquí: http://productarchitect.com/code/better-natural-sort.py

Comentarios bienvenidos. Eso no pretende ser una solución definitiva; solo un paso adelante

Scott Lawton
fuente
En su script de prueba al que vincula, natsortedy humansortedfalla porque se usaron incorrectamente ... intentó pasar natsortedcomo clave pero en realidad es la función de clasificación en sí. Deberías haberlo intentado natsort_keygen().
SethMMorton
2

Lo más probable functools.cmp_to_key()es que esté estrechamente relacionado con la implementación subyacente del género Python. Además, el parámetro cmp es heredado. La forma moderna es transformar los elementos de entrada en objetos que admitan las operaciones de comparación enriquecidas deseadas.

Bajo CPython 2.x, se pueden ordenar objetos de tipos dispares incluso si los respectivos operadores de comparación enriquecida no se han implementado. En CPython 3.x, los objetos de diferentes tipos deben admitir explícitamente la comparación. Consulte ¿Cómo compara Python string e int? que enlaza con la documentación oficial . La mayoría de las respuestas dependen de este orden implícito. Cambiar a Python 3.x requerirá un nuevo tipo para implementar y unificar las comparaciones entre números y cadenas.

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

Hay tres enfoques diferentes. El primero usa clases anidadas para aprovechar el Iterablealgoritmo de comparación de Python . El segundo desenrolla este anidamiento en una sola clase. El tercero renuncia a la subclase strpara centrarse en el rendimiento. Todos son cronometrados; el segundo es dos veces más rápido, mientras que el tercero es casi seis veces más rápido. strNo se requiere subclasificar , y probablemente fue una mala idea en primer lugar, pero viene con ciertas comodidades.

Los caracteres de clasificación se duplican para forzar el pedido por mayúsculas y se intercambian entre mayúsculas y minúsculas para forzar que las letras minúsculas se ordenen primero; Esta es la definición típica de "género natural". No pude decidir sobre el tipo de agrupación; algunos podrían preferir lo siguiente, que también trae importantes beneficios de rendimiento:

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

Cuando se utilizan, los operadores de comparación se configuran para que objectno sean 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

La ordenación natural es bastante complicada y vagamente definida como un problema. No olvides correr de unicodedata.normalize(...)antemano, y considera el uso en str.casefold()lugar de hacerlo str.lower(). Probablemente hay problemas sutiles de codificación que no he considerado. Por lo tanto, recomiendo tentativamente la biblioteca natsort . Eché un vistazo rápido al repositorio de github; El mantenimiento del código ha sido estelar.

Todos los algoritmos que he visto dependen de trucos como duplicar y bajar caracteres, y cambiar mayúsculas y minúsculas. Si bien esto duplica el tiempo de ejecución, una alternativa requeriría un orden natural total en el conjunto de caracteres de entrada. No creo que esto sea parte de la especificación Unicode, y dado que hay muchos más dígitos Unicode que [0-9], crear tal clasificación sería igualmente desalentador. Si desea realizar comparaciones locales, prepare sus cadenas con el locale.strxfrmmétodo de clasificación de Python .

user19087
fuente
1

Permítanme presentar mi propia opinión sobre esta necesidad:

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

Ahora si tenemos la lista como tal:

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

Simplemente podemos usar el key=kwarg para hacer un tipo natural:

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

El inconveniente aquí es, por supuesto, como lo es ahora, la función clasificará las letras mayúsculas antes que las minúsculas.

Dejaré la implementación de un mero que no distingue entre mayúsculas y minúsculas para el lector :-)

pepoluan
fuente
0

Le sugiero que simplemente use el keyargumento de palabra clave de sortedpara lograr su lista deseada.
Por ejemplo:

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
fuente
1
Esto no maneja dígitos. a_51sería después a500, aunque 500> 51
skjerns
Es cierto, mi respuesta simplemente coincide con el ejemplo dado de Elm11 y elm1. Perdí la solicitud de clasificación natural específicamente y la respuesta marcada es probablemente la mejor aquí :)
Johny Vaknin
0

Siguiendo la respuesta de @Mark Byers, aquí hay una adaptación que acepta el keyparámetro y es más compatible con 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)

También hice un Gist

Edouardtheron
fuente
(-1) esta respuesta no trae nada nuevo en comparación con la de Mark (cualquier linter puede PEP8-ify algún código). O tal vez el keyparámetro? Pero esto también se ejemplifica en la respuesta de @ beauburrier
Ciprian Tomoiagă
0

Una mejora en la mejora de Claudiu en la respuesta 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)

Por cierto, tal vez no todos recuerden que los valores predeterminados del argumento de la función se evalúan en el defmomento

Walter Tross
fuente
-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

Agradecimientos :

Tarea tipo burbuja

Cómo leer una cadena letra por letra en python

Varadaraju G
fuente
-2
>>> import re
>>> sorted(lst, key=lambda x: int(re.findall(r'\d+$', x)[0]))
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
SilentGhost
fuente
44
Su implementación solo resuelve el problema de los números. La implementación falla si las cadenas no tienen números. Pruébelo en ['silencio', 'fantasma'] por ejemplo (índice de lista fuera de rango).
snakile
2
@snaklie: su pregunta no puede proporcionar un caso de ejemplo decente. No ha explicado lo que está tratando de hacer, y tampoco ha actualizado su pregunta con esta nueva información. No has publicado nada que hayas intentado, así que por favor no seas tan desdeñoso con mi intento de telepatía.
SilentGhost
55
@SilentGhost: Primero, te di un voto positivo porque creo que tu respuesta es útil (aunque no resuelva mi problema). Segundo, no puedo cubrir todos los casos posibles con ejemplos. Creo que he dado una definición bastante clara al tipo natural. No creo que sea una buena idea dar un ejemplo complejo o una definición larga a un concepto tan simple. Puede editar mi pregunta si puede pensar en una mejor formulación del problema.
snakile
1
@SilentGhost: me gustaría tratar con tales cadenas de la misma manera que Windows trata con dichos nombres de archivo cuando clasifica los archivos por nombre (ignorar casos, etc.). Me parece claro, pero todo lo que digo me parece claro, así que no debo juzgar si está claro o no.
snakile
1
@snakile no has estado cerca de definir la búsqueda natural. Eso sería bastante difícil de hacer y requeriría muchos detalles. Si desea el orden de clasificación utilizado por el explorador de Windows, ¿sabe que hay una simple llamada API que proporciona esto?
David Heffernan el