Есть ли встроенная функция для естественной сортировки строк?

282

Используя Python 3.x, у меня есть список строк, для которых я хотел бы выполнить естественную сортировку по алфавиту.

Естественная сортировка: порядок сортировки файлов в Windows.

Например, следующий список естественно отсортирован (что я хочу):

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

И вот «отсортированная» версия приведенного выше списка (что у меня есть):

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

Я ищу функцию сортировки, которая ведет себя как первая.

snakile
источник
13
Определение естественной сортировки не является "порядком сортировки файлов в Windows".
Гленн Мейнард
Все ответы на этом сайте приведут к неверным результатам, если вы хотите сортировать по типу Windows Explorer , например, сортировку !1, 1, !a, a. Похоже, что единственный способ получить сортировку, подобный Windows, - это использовать StrCmpLogicalW саму функцию Windows , поскольку никто, кажется, не реализовал эту функцию правильно (источник был бы признателен). Решение: stackoverflow.com/a/48030307/2441026
user136036

Ответы:

236

Для этого в PyPI есть сторонняя библиотека natsort (полное раскрытие, я автор пакета). Для вашего случая вы можете выполнить одно из следующих действий:

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

Вы должны заметить, что он natsortиспользует общий алгоритм, поэтому он должен работать практически со всеми входными данными, которые вы к нему добавляете. Если вы хотите получить более подробную информацию о том, почему вы можете выбрать библиотеку для этого, а не использовать собственную функцию, посетите страницу natsortдокументации, как это работает , в частности, « Особые случаи везде»! раздел.


Если вам нужен ключ сортировки вместо функции сортировки, используйте одну из следующих формул.

>>> 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
источник
5
Я также думаю, что довольно интересно, что natsort также сортирует правильно, когда число не в конце: как это часто бывает для имен файлов. Не стесняйтесь включать следующий пример: pastebin.com/9cwCLdEK
Мартин Тома
1
Natsort - отличная библиотека, ее следует добавить в стандартную библиотеку Python! :-)
Митч МакМаберс
natsortтакже «естественно» обрабатывает случай нескольких отдельных чисел в строках. Отличный материал!
FlorianH
182

Попробуй это:

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)

Вывод:

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

Код, адаптированный здесь: Сортировка для людей: Порядок естественной сортировки .

Марк Байерс
источник
2
почему вы используете return sorted(l, key)вместо l.sort(key)? Это для увеличения производительности или просто чтобы быть более питоническим?
Джерелли
12
@jperelli Я думаю, что лестница изменит исходный список в вызывающем абоненте. Но, скорее всего, звонящий хочет получить еще одну мелкую копию списка.
Хагги
3
Только для записи, это не может обрабатывать все входные данные: разделение str / int должно выстраиваться, иначе вы создадите сравнения, такие как ["foo", 0] <[0, "foo"] для ввода ["foo0 "," 0foo "], что вызывает ошибку TypeError.
user19087
4
@ user19087: На самом деле это работает, потому что re.split('([0-9]+)', '0foo')возвращается ['', '0', 'foo']. Из-за этого строки всегда будут на четных индексах и целые на нечетных индексах в массиве.
Флориан
Для тех, кто интересуется производительностью, это заметно медленнее, чем родной тип Python. т.е. в 25-50 раз медленнее. И если вы хотите всегда надежно сортировать [elm1, elm2, Elm2, elm2] как [elm1, Elm2, elm2, elm2] (заглавными буквами перед нижним), то вы можете просто вызвать natural_sort (sorted (lst)). Более неэффективно, но очень легко получить повторяемую сортировку. Скомпилируйте регулярное выражение для ускорения ~ 50%. как видно из ответа Клавдия.
Чарли Хейли
100

Вот гораздо более питонная версия ответа Марка Байера:

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

Теперь эта функция может быть использована в качестве ключа в любой функции , которая использует его, как list.sort, sorted, maxи т.д.

Как лямбда

lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', s)]
Клаудиу
источник
9
Модуль re автоматически компилирует и кэширует регулярные выражения, поэтому прекомпиляция не требуется
wim
1
@wim: он кэширует последние использования X, так что технически возможно использовать регулярные выражения X + 5, а затем выполнять естественную сортировку снова и снова, после чего это не будет кэшироваться. но, вероятно, незначительный в долгосрочной перспективе
Claudiu
Я этого не делал, но, возможно, причина в том, что он не может обрабатывать кортежи, как обычная сортировка Python.
Несчастный Кот
1
X использования, упомянутые @Claudiu, кажутся 100 на Python 2.7 и 512 на Python 3.4. Также обратите внимание, что при достижении лимита кеш полностью очищается (поэтому выбрасывается не только самый старый).
Цитракс
@Zitrax Почему / Как имеет смысл полностью очистить кеш?
Джошуа
19

Я написал функцию, основанную на http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html, которая добавляет возможность по-прежнему передавать свой собственный параметр «ключ». Мне это нужно для того, чтобы выполнять естественную сортировку списков, которые содержат более сложные объекты (не только строки).

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)

Например:

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
источник
более простой способ сделать это - определить natural_sort_key, а затем при сортировке списка вы можете связать ключи, например:list.sort(key=lambda el: natural_sort_key(el['name']))
Claudiu
17
data = ['elm13', 'elm9', 'elm0', 'elm1', 'Elm11', 'Elm2', 'elm10']

Давайте проанализируем данные. Разрядность всех элементов равна 2. И есть 3 буквы в общей буквенной части'elm' .

Таким образом, максимальная длина элемента равна 5. Мы можем увеличить это значение, чтобы убедиться (например, до 8).

Учитывая это, у нас есть однострочное решение:

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

без регулярных выражений и внешних библиотек!

print(data)

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

Объяснение:

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

>>>
0000elm0
0000elm1
0000elm2
0000elm9
000elm10
000elm11
000elm13
Серго
источник
1
Это не обрабатывает данные динамической / неизвестной длины. Это также сортирует по-другому, чем другие решения для данных, у которых есть числа в данных, против которых в конце. * Это не обязательно нежелательно, но я думаю, что стоит отметить.
JerodG
1
Если вам нужно обработать данные динамической длины, вы можете использовать их width = max(data, key=len)для вычисления того, что нужно 8'{0:0>{width}}'.format(x, width=width)
добавить
1
Просто выполнив тест по времени по сравнению со всеми другими на этом форуме, это решение на сегодняшний день является самым быстрым и наиболее эффективным для того типа данных, который пытается обработать @snakile
SR Colledge
13

Дано:

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

Подобно решению SergO, 1-вкладыш без внешних библиотек будет :

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

или

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

Объяснение:

Это решение использует ключевую особенность сортировки чтобы определить функцию, которая будет использоваться для сортировки. Поскольку мы знаем, что каждой записи данных предшествует 'elm', функция сортировки преобразует целую часть строки после 3-го символа (т. Е. Int (x [3:])). Если числовая часть данных находится в другом месте, то эту часть функции придется изменить.

ура

Камило
источник
6
А теперь для чего-то более * элегантного (питонического) просто прикосновение

Существует множество реализаций, и, хотя некоторые из них приблизились, ни одна из них не отразила элегантность, которую предоставляет современный питон.

  • Протестировано с использованием Python (3.5.1)
  • Включен дополнительный список, чтобы продемонстрировать, что он работает, когда числа находятся в середине строки
  • Не проверял, однако, я предполагаю, что если бы ваш список был значительным, было бы более эффективно предварительно скомпилировать регулярное выражение
    • Я уверен, что кто-то исправит меня, если это ошибочное предположение

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)])
Полный код
#!/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']

Осторожно при использовании

  • from os.path import split
    • вам нужно будет дифференцировать импорт

Вдохновение от

JerodG
источник
6

Значение этого поста

Я хочу предложить решение без регулярных выражений, которое можно применять в целом.
Я создам три функции:

  1. find_first_digitкоторый я позаимствовал у @AnuragUniyal . Он найдет позицию первой цифры или не цифры в строке.
  2. split_digitsкоторый является генератором, который разбирает строку на цифры и не цифры. Он также будет yieldцелым числом, когда это цифра.
  3. natural_keyпросто заворачивает split_digitsв tuple. Это то , что мы используем в качестве ключа для sorted, max, min.

функции

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

Мы можем видеть, что в общем случае мы можем иметь несколько цифр:

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

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

Или оставьте с учетом регистра:

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

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

Мы видим, что он сортирует список ОП в соответствующем порядке.

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

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

Но он может обрабатывать и более сложные списки:

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

Мой эквивалент регулярного выражения будет

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
источник
1
Большое спасибо! Однако я хочу добавить, что если у вас есть «12345_A» и «12345_A2», последние будут отсортированы до первого. По крайней мере, это не так, как это делает Windows. Тем не менее, все еще работает для вышеуказанной проблемы!
morph3us
4

Один из вариантов - превратить строку в кортеж и заменить цифры, используя расширенную форму http://wiki.answers.com/Q/What_does_expanded_form_mean

таким образом a90 станет ("a", 90,0) и a1 станет ("a", 1)

ниже приведен пример кода (который не очень эффективен из-за способа удаления начальных 0 из чисел)

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)

вывод:

('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
К сожалению, это решение работает только для Python 2.X. Для Python 3 ('b', 1) < ('b', 'e', 't', 'a', 1, '.', 1)вернетсяTypeError: unorderable types: int() < str()
SethMMorton
@SethMMorgon прав, этот код легко ломается в Python 3. Казалось бы natsort, естественная альтернатива , pypi.org/project/natsort
FlorianH
3

Основываясь на ответах здесь, я написал natural_sorted функцию, которая ведет себя как встроенная функция 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)

Исходный код также доступен в моем хранилище фрагментов GitHub: https://github.com/bdrung/snippets/blob/master/natural_sorted.py

Бенджамин Друнг
источник
2

Приведенные выше ответы хороши для показанного конкретного примера , но пропускают несколько полезных случаев для более общего вопроса естественной сортировки. Я только что попал в один из этих случаев, поэтому создал более тщательное решение:

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

Тестовый код и несколько ссылок (вкл и выкл StackOverflow) находятся здесь: http://productarchitect.com/code/better-natural-sort.py

Обратная связь приветствуется. Это не должно быть окончательным решением; просто шаг вперед.

Скотт Лоутон
источник
В вашем тестовом скрипте, на который вы ссылаетесь, natsortedи humansortedтерпите неудачу, потому что они использовались неправильно ... вы пытались передать его natsortedкак ключ, но на самом деле это была сама функция сортировки. Вы должны были попробовать natsort_keygen().
СетМортон
2

Скорее всего, functools.cmp_to_key()он тесно связан с базовой реализацией сортировки Python. Кроме того, параметр cmp является устаревшим. Современный способ заключается в преобразовании входных элементов в объекты, которые поддерживают требуемые операции расширенного сравнения.

В CPython 2.x объекты разнородных типов могут быть упорядочены, даже если соответствующие операторы расширенного сравнения не были реализованы. В CPython 3.x объекты разных типов должны явно поддерживать сравнение. Посмотрите, как Python сравнивает строку и int? какие ссылки на официальную документацию . Большинство ответов зависят от этого неявного порядка. Переход на Python 3.x потребует нового типа для реализации и унификации сравнений между числами и строками.

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

Есть три разных подхода. Первый использует вложенные классы, чтобы использовать преимущества Iterableалгоритма сравнения Python . Второй разворачивает это вложение в один класс. Третий отказывается от подклассов, strчтобы сосредоточиться на производительности. Все рассчитано; второй в два раза быстрее, а третий почти в шесть раз быстрее. Подклассы strне требуются, и, вероятно, изначально были плохой идеей, но они имеют определенные удобства.

Символы сортировки дублируются, чтобы принудительно упорядочить регистр, и меняются регистром для принудительной сортировки букв нижнего регистра; это типичное определение «натуральный сорт». Я не мог определиться с типом группировки; некоторые могут предпочесть следующее, что также дает значительные преимущества в производительности:

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

Там, где они используются, операторы сравнения устанавливаются так, что objectони не будут игнорироватьсяfunctools.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

Естественная сортировка довольно сложна и неопределенно определяется как проблема. Не забудьте запустить unicodedata.normalize(...)заранее, и рассмотреть вопрос об использовании, str.casefold()а не str.lower(). Возможно, есть тонкие проблемы с кодированием, которые я не рассматривал. Поэтому я рекомендую библиотеку natsort . Я быстро взглянул на хранилище github; обслуживание кода было звездным.

Все алгоритмы, которые я видел, зависят от таких хитростей, как дублирование и понижение символов, а также от случая замены. Хотя это удваивает время выполнения, альтернатива потребует полного естественного упорядочения во входном наборе символов. Я не думаю, что это является частью спецификации Unicode, и так как у Unicode гораздо больше цифр, чем [0-9]создание такой сортировки, было бы одинаково сложно. Если вы хотите сравнения с locale.strxfrmучетом локали, подготовьте свои строки в соответствии с сортировкой Python HOW TO .

user19087
источник
1

Позвольте мне представить свой взгляд на эту потребность:

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

Теперь, если у нас есть такой список:

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

Мы можем просто использовать key=kwarg для естественной сортировки:

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

Недостаток здесь, конечно, как и сейчас, функция будет сортировать заглавные буквы перед строчными.

Я оставлю реализацию нечувствительного к регистру групера читателю :-)

pepoluan
источник
0

Я предлагаю вам просто использовать keyключевое слово аргумент sortedдля достижения желаемого списка.
Например:

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
это не обрабатывает цифры. a_51будет после a500, хотя 500> 51
skjerns
Правда, мой ответ просто соответствует приведенному примеру Elm11 и elm1. Специально пропустил запрос на естественную сортировку, и помеченный ответ, вероятно, лучший здесь :)
Джонни Вакнин
0

После ответа @Mark Byers приведена адаптация, которая принимает этот keyпараметр и является более совместимой с 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)

Я также сделал Gist

edouardtheron
источник
(-1) этот ответ не приносит ничего нового по сравнению с ответом Марка (любой линтер может PEP8-если-нибудь код). Или, может быть, keyпараметр? Но это также иллюстрируется в ответе @ beauburrier
Ciprian Tomoiagă
0

Улучшение в улучшении Клаудиу по ответу Марка Байера ;-)

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)

Кстати, может быть, не все помнят, что значения по умолчанию для аргументов функции оцениваются по defвремени

Уолтер Тросс
источник
-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

Благодарности :

Bubble Sort Домашнее задание

Как читать строку по одной букве за раз в Python

Варадараджу Г
источник
-2
>>> import re
>>> sorted(lst, key=lambda x: int(re.findall(r'\d+$', x)[0]))
['elm0', 'elm1', 'Elm2', 'elm9', 'elm10', 'Elm11', 'Elm12', 'elm13']
SilentGhost
источник
4
Ваша реализация решает только проблему с числами. Реализация заканчивается неудачей, если в строках нет чисел. Попробуйте, например, [тишина, призрак] (индекс списка вне диапазона).
snakile
2
@snaklie: ваш вопрос не может служить достойным примером. Вы не объяснили, что вы пытаетесь сделать, и не обновили свой вопрос этой новой информацией. Вы не опубликовали ничего, что вы попробовали, поэтому, пожалуйста, не пренебрегайте моей попыткой телепатии.
SilentGhost
5
@SilentGhost: Во-первых, я дал вам ответ, потому что я думаю, что ваш ответ полезен (хотя он не решает мою проблему). Во-вторых, я не могу охватить все возможные случаи примерами. Я думаю, что я дал довольно четкое определение натуральной сортировке. Я не думаю, что было бы хорошей идеей дать сложный пример или длинное определение такой простой концепции. Вы можете отредактировать мой вопрос, если сможете придумать лучшую формулировку проблемы.
snakile
1
@SilentGhost: я хотел бы иметь дело с такими строками так же, как Windows работает с такими именами файлов, когда сортирует файлы по имени (игнорировать случаи и т. Д.). Это кажется мне ясным, но все, что я говорю, кажется мне ясным, поэтому я не могу судить, ясно это или нет.
snakile
1
@snakile вы не приблизились к определению естественного поиска. Это было бы довольно сложно сделать и потребовало бы много деталей. Если вы хотите порядок сортировки, используемый проводником Windows, знаете ли вы, что существует простой вызов API, который обеспечивает это?
Дэвид Хеффернан