Existe-t-il une fonction intégrée pour le tri naturel des chaînes?

282

En utilisant Python 3.x, j'ai une liste de chaînes pour lesquelles je voudrais effectuer un tri alphabétique naturel.

Tri naturel: ordre de tri des fichiers dans Windows.

Par exemple, la liste suivante est naturellement triée (ce que je veux):

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

Et voici la version "triée" de la liste ci-dessus (ce que j'ai):

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

Je recherche une fonction de tri qui se comporte comme la première.

snakile
la source
13
La définition d'un tri naturel n'est pas "l'ordre dans lequel Windows trie les fichiers".
Glenn Maynard
Toutes les réponses sur ce site produiront des résultats incorrects si vous souhaitez un tri de type "Explorateur Windows" dans plusieurs cas, par exemple un tri !1, 1, !a, a. La seule façon d'obtenir un tri comme Windows semble être d'utiliser la StrCmpLogicalW fonction Windows elle-même, car personne ne semble avoir correctement réimplémenté cette fonction (la source serait appréciée). Solution: stackoverflow.com/a/48030307/2441026
user136036

Réponses:

236

Il existe une bibliothèque tierce pour cela sur PyPI appelée natsort (divulgation complète, je suis l'auteur du package). Pour votre cas, vous pouvez effectuer l'une des opérations suivantes:

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

Vous devez noter qu'il natsortutilise un algorithme général, il devrait donc fonctionner pour à peu près toutes les entrées que vous lui lancez. Si vous souhaitez plus de détails sur les raisons pour lesquelles vous pouvez choisir une bibliothèque pour ce faire plutôt que de lancer votre propre fonction, consultez la page Comment ça marche de la natsortdocumentation , en particulier les cas spéciaux partout! section.


Si vous avez besoin d'une clé de tri au lieu d'une fonction de tri, utilisez l'une des formules ci-dessous.

>>> 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
la source
5
Je pense aussi qu'il est assez intéressant que natsort trie également correctement lorsque le nombre n'est pas à la fin: comme c'est souvent le cas pour les noms de fichiers. N'hésitez pas à inclure l'exemple suivant: pastebin.com/9cwCLdEK
Martin Thoma
1
Natsort est une excellente bibliothèque, devrait être ajoutée à la bibliothèque standard de python! :-)
Mitch McMabers
natsortgère également «naturellement» le cas de plusieurs nombres séparés dans les chaînes. Super truc!
FlorianH
182

Essaye ça:

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)

Production:

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

Code adapté d'ici: Tri pour les humains: Ordre de tri naturel .

Mark Byers
la source
2
pourquoi utilisez-vous return sorted(l, key)au lieu de l.sort(key)? Est-ce pour un gain de performance ou simplement pour être plus pythonique?
jperelli
12
@jperelli Je pense que l'échelle changerait la liste d'origine dans l'appelant. Mais très probablement, l'appelant veut une autre copie superficielle de la liste.
huggie
3
Juste pour mémoire, cela ne peut pas gérer toutes les entrées: les séparations str / int doivent s'aligner, sinon vous créerez des comparaisons comme ["foo", 0] <[0, "foo"] pour l'entrée ["foo0 "," 0foo "], qui déclenche une TypeError.
user19087
4
@ user19087: En fait ça marche, car ça re.split('([0-9]+)', '0foo')revient ['', '0', 'foo']. Pour cette raison, les chaînes seront toujours sur les index pairs et les entiers sur les index impairs dans le tableau.
Florian Kusche
Pour quiconque s'interroge sur les performances, cela est notamment plus lent que le tri natif de python. soit 25 -50x plus lent. Et si vous souhaitez toujours trier [elm1, elm2, Elm2, elm2] comme [elm1, Elm2, elm2, elm2] de manière fiable (majuscules avant le bas), vous pouvez simplement appeler natural_sort (sorted (lst)). Plus inefficace, mais très facile d'obtenir un tri reproductible. Compilez l'expression régulière pour une accélération de ~ 50%. comme on le voit dans la réponse de Claudiu.
Charlie Haley
100

Voici une version beaucoup plus pythonique de la réponse 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)]    

Maintenant , cette fonction peut être utilisée comme une clé dans une fonction qui l' utilise, comme list.sort, sorted, max, etc.

En tant que lambda:

lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]
Claudiu
la source
9
Le module compile et met en cache les expressions rationnelles de manière automatique, il n'est donc pas nécessaire de précompiler
wim
1
@wim: il met en cache les dernières utilisations de X, il est donc techniquement possible d'utiliser des expressions régulières X + 5, puis de faire un tri naturel encore et encore, auquel cas cela ne serait pas mis en cache. mais probablement négligeable à long terme
Claudiu
Je ne l'ai pas fait, mais peut-être la raison en est qu'il ne peut pas gérer les tuples, comme un tri python normal.
The Unfun Cat
1
Les utilisations X mentionnées par @Claudiu semblent être 100 sur Python 2.7 et 512 sur Python 3.4. Et notez également que lorsque la limite est atteinte, le cache est complètement vidé (ce n'est donc pas seulement le plus ancien qui est rejeté).
Zitrax
@Zitrax Pourquoi / Comment est-il judicieux de vider complètement le cache?
Joschua
19

J'ai écrit une fonction basée sur http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html qui ajoute la possibilité de toujours passer votre propre paramètre «clé». J'en ai besoin pour effectuer une sorte naturelle de listes qui contiennent des objets plus complexes (pas seulement des chaînes).

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)

Par exemple:

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
la source
une façon plus simple de le faire serait de définir natural_sort_key, puis lors du tri d'une liste, vous pourriez faire la chaîne de vos clés, par exemple:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu
17
data = ['elm13', 'elm9', 'elm0', 'elm1', 'Elm11', 'Elm2', 'elm10']

Analysons les données. La capacité numérique de tous les éléments est de 2. Et il y a 3 lettres dans la partie littérale commune 'elm'.

Ainsi, la longueur maximale de l'élément est 5. Nous pouvons augmenter cette valeur pour nous en assurer (par exemple, à 8).

Gardant cela à l'esprit, nous avons une solution en ligne:

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

sans expressions régulières et bibliothèques externes!

print(data)

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

Explication:

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

>>>
0000elm0
0000elm1
0000elm2
0000elm9
000elm10
000elm11
000elm13
SergO
la source
1
Cela ne gère pas les données de longueur dynamique / inconnue. Il trie également différemment des autres solutions pour les données qui ont des nombres dans les données opposées à la fin. * Ce n'est pas nécessairement indésirable mais je pense qu'il est bon de le souligner.
JerodG
1
Si vous avez besoin de gérer des données de longueur dynamique, vous pouvez utiliser width = max(data, key=len)pour calculer quoi sous8'{0:0>{width}}'.format(x, width=width)
tendre
1
Juste en faisant un test chronométré par rapport à tous les autres sur ce forum, cette solution est de loin la plus rapide et la plus efficace pour le type de données que @snakile essaie de traiter
SR Colledge
13

Donné:

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

Semblable à la solution de SergO, un liner sans bibliothèques externes serait :

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

ou

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

Explication:

Cette solution utilise la fonctionnalité clé de tri pour définir une fonction qui sera utilisée pour le tri. Parce que nous savons que chaque entrée de données est précédée de «elm», la fonction de tri convertit en entier la partie de la chaîne après le 3ème caractère (c'est-à-dire int (x [3:])). Si la partie numérique des données se trouve à un emplacement différent, alors cette partie de la fonction devrait changer.

À votre santé

Camilo
la source
6
Et maintenant pour quelque chose de plus * élégant (pythonique) - juste une touche

Il existe de nombreuses implémentations, et bien que certaines se soient rapprochées, aucune n'a capturé l'élégance offerte par le python moderne.

  • Testé en python (3.5.1)
  • Inclus une liste supplémentaire pour démontrer que cela fonctionne lorsque les chiffres sont à mi-chaîne
  • Je n'ai pas testé, cependant, je suppose que si votre liste était assez importante, il serait plus efficace de compiler au préalable l'expression régulière.
    • Je suis sûr que quelqu'un me corrigera si c'est une hypothèse erronée

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)])
Code complet
#!/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']

Attention lors de l'utilisation

  • from os.path import split
    • vous devrez différencier les importations

Inspiration de

JerodG
la source
6

Valeur de ce message

Mon point est d'offrir une solution non regex qui peut être appliquée de manière générale.
Je vais créer trois fonctions:

  1. find_first_digitque j'ai emprunté à @AnuragUniyal . Il trouvera la position du premier chiffre ou non numérique dans une chaîne.
  2. split_digitsqui est un générateur qui sélectionne une chaîne en morceaux numériques et non numériques. Il sera également yieldentier lorsqu'il s'agit d'un chiffre.
  3. natural_keys'enroule juste split_digitsdans un tuple. C'est ce que nous utilisons comme clé pour sorted, max, min.

Les fonctions

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

Nous pouvons voir qu'il est général en ce sens que nous pouvons avoir des morceaux à plusieurs chiffres:

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

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

Ou laissez en respectant la casse:

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

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

Nous pouvons voir qu'il trie la liste des PO dans l'ordre approprié

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

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

Mais il peut également gérer des listes plus compliquées:

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

Mon équivalent regex serait

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
la source
1
Merci beaucoup! Je veux cependant ajouter que si vous avez "12345_A" et "12345_A2", ces derniers seront triés avant le premier. Ce n'est du moins pas ainsi que Windows le fait. Fonctionne toujours pour le problème ci-dessus, cependant!
morph3us
4

Une option consiste à transformer la chaîne en un tuple et à remplacer les chiffres à l'aide du formulaire étendu http://wiki.answers.com/Q/What_does_expanded_form_mean

de cette façon, a90 deviendrait ("a", 90,0) et a1 deviendrait ("a", 1)

ci-dessous est un exemple de code (qui n'est pas très efficace en raison de la façon dont il supprime les 0 en tête des nombres)

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)

production:

('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
la source
1
Malheureusement, cette solution ne fonctionne que pour Python 2.X. Pour Python 3, ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)reviendraTypeError: unorderable types: int() < str()
SethMMorton
@SethMMorgon a raison, ce code casse facilement en Python 3. L'alternative naturelle semble natsort, pypi.org/project/natsort
FlorianH
3

Sur la base des réponses ici, j'ai écrit une natural_sortedfonction qui se comporte comme la fonction intégrée 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)

Le code source est également disponible dans mon référentiel d'extraits de GitHub: https://github.com/bdrung/snippets/blob/master/natural_sorted.py

Benjamin Drung
la source
2

Les réponses ci-dessus sont bonnes pour l' exemple spécifique qui a été montré, mais manquent plusieurs cas utiles pour la question plus générale du type naturel. Je viens de mordre par l'un de ces cas, j'ai donc créé une solution plus approfondie:

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

Le code de test et plusieurs liens (sur et hors de StackOverflow) sont ici: http://productarchitect.com/code/better-natural-sort.py

Commentaires bienvenus. Ce n'est pas censé être une solution définitive; juste un pas en avant.

Scott Lawton
la source
Dans votre script de test auquel vous vous connectez, natsortedet humansortedéchouez parce qu'ils ont été mal utilisés ... vous avez essayé de passer natsortedcomme clé mais c'est en fait la fonction de tri elle-même. Vous auriez dû essayer natsort_keygen().
SethMMorton
2

Le plus probable functools.cmp_to_key()est étroitement lié à l'implémentation sous-jacente du tri de python. De plus, le paramètre cmp est hérité. La méthode moderne consiste à transformer les éléments d'entrée en objets qui prennent en charge les opérations de comparaison riches souhaitées.

Sous CPython 2.x, des objets de types disparates peuvent être ordonnés même si les opérateurs de comparaison riche respectifs n'ont pas été implémentés. Sous CPython 3.x, les objets de différents types doivent explicitement prendre en charge la comparaison. Voir Comment Python compare-t-il string et int? qui renvoie à la documentation officielle . La plupart des réponses dépendent de cet ordre implicite. Le passage à Python 3.x nécessitera un nouveau type pour implémenter et unifier les comparaisons entre les nombres et les chaînes.

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

Il existe trois approches différentes. Le premier utilise des classes imbriquées pour tirer parti de l' Iterablealgorithme de comparaison de Python . La seconde déroule cette imbrication en une seule classe. Le troisième renonce au sous-classement strpour se concentrer sur la performance. Tous sont chronométrés; le second est deux fois plus rapide tandis que le troisième presque six fois plus rapide. Le sous str- classement n'est pas requis, et c'était probablement une mauvaise idée en premier lieu, mais cela vient avec certaines commodités.

Les caractères de tri sont dupliqués pour forcer le tri par casse et permutés pour forcer la lettre minuscule à trier en premier; c'est la définition typique du "tri naturel". Je ne pouvais pas décider du type de regroupement; certains pourraient préférer ce qui suit, ce qui apporte également des avantages de performances significatifs:

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

Lorsqu'ils sont utilisés, les opérateurs de comparaison sont définis sur celui de objectsorte qu'ils ne seront pas ignorés parfunctools.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

Le tri naturel est à la fois assez compliqué et vaguement défini comme un problème. N'oubliez pas de courir unicodedata.normalize(...)avant et pensez à utiliser str.casefold()plutôt qu'à str.lower(). Il y a probablement des problèmes d'encodage subtils que je n'ai pas pris en compte. Je recommande donc provisoirement la bibliothèque natsort . J'ai jeté un rapide coup d'œil au dépôt github; la maintenance du code a été stellaire.

Tous les algorithmes que j'ai vus dépendent d'astuces telles que la duplication et la réduction de caractères et l'échange de casse. Bien que cela double le temps d'exécution, une alternative nécessiterait un ordre naturel total sur le jeu de caractères d'entrée. Je ne pense pas que cela fasse partie de la spécification unicode, et comme il y a beaucoup plus de chiffres unicode que [0-9], créer un tel tri serait tout aussi intimidant. Si vous voulez des comparaisons locales, préparez vos chaînes avec locale.strxfrmle tri de Python COMMENT FAIRE .

user19087
la source
1

Permettez-moi de soumettre ma propre opinion sur ce besoin:

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

Maintenant, si nous avons la liste comme telle:

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

Nous pouvons simplement utiliser le key=kwarg pour faire un tri naturel:

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

L'inconvénient ici est bien sûr, comme c'est le cas actuellement, la fonction triera les lettres majuscules avant les lettres minuscules.

Je laisse au lecteur l'implémentation d'un mérou insensible à la casse :-)

pepoluan
la source
0

Je vous suggère simplement d'utiliser l' keyargument mot clé de sortedpour obtenir la liste souhaitée.
Par exemple:

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
la source
1
cela ne gère pas les chiffres. a_51serait après a500, bien que 500> 51
skjerns
Certes, ma réponse correspond simplement à l'exemple donné de Elm11 et elm1. Vous avez manqué la demande de tri naturel en particulier et la réponse marquée est probablement la meilleure ici :)
Johny Vaknin
0

Suite à la réponse de @Mark Byers, voici une adaptation qui accepte le keyparamètre, et est plus conforme au 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)

J'ai aussi fait un Gist

Edouardtheron
la source
(-1) cette réponse n'apporte rien de nouveau par rapport à Mark (tout linter peut PEP8-ify un certain code). Ou peut-être le keyparamètre? Mais cela est également illustré dans la réponse de @ beauburrier
Ciprian Tomoiagă
0

Une amélioration par rapport à l'amélioration de Claudiu par rapport à la réponse 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, peut-être que tout le monde ne se souvient pas que les valeurs par défaut des arguments de fonction sont évaluées au defmoment

Walter Tross
la source
-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

Remerciements :

Bubble Sort Devoirs

Comment lire une chaîne une lettre à la fois en python

Varadaraju G
la source
-2
>>> import re
>>> sorted(lst, key=lambda x: int(re.findall(r'\d+$', x)[0]))
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
SilentGhost
la source
4
Votre implémentation ne résout que le problème des nombres. L'implémentation échoue si les chaînes ne contiennent pas de nombres. Essayez-le sur ['silent', 'ghost'] par exemple (liste des index hors limites).
snakile
2
@snaklie: votre question ne fournit pas d'exemple décent. Vous n'avez pas expliqué ce que vous essayez de faire et vous n'avez pas non plus mis à jour votre question avec ces nouvelles informations. Vous n'avez rien publié que vous ayez essayé, alors s'il vous plaît ne soyez pas si dédaigneux de ma tentative de télépathie.
SilentGhost
5
@SilentGhost: Tout d'abord, je vous ai donné un vote positif parce que je pense que votre réponse est utile (même si cela ne résout pas mon problème). Deuxièmement, je ne peux pas couvrir tous les cas possibles avec des exemples. Je pense avoir donné une définition assez claire du tri naturel. Je ne pense pas que ce soit une bonne idée de donner un exemple complexe ou une longue définition à un concept aussi simple. Vous êtes invités à modifier ma question si vous pouvez penser à une meilleure formulation du problème.
snakile
1
@ SilentGhost: Je voudrais traiter ces chaînes de la même manière que Windows traite ces noms de fichiers lorsqu'il trie les fichiers par nom (ignorer les cas, etc.). Cela me semble clair, mais tout ce que je dis me semble clair, donc je ne dois pas juger si c'est clair ou non.
snakile
1
@snakile, vous êtes loin de définir de près la recherche naturelle. Ce serait assez difficile à faire et nécessiterait beaucoup de détails. Si vous voulez l'ordre de tri utilisé par l'explorateur Windows, savez-vous qu'il existe un simple appel api qui le fournit?
David Heffernan