Чи є вбудована функція для рядкового природного сортування?


281

Використовуючи Python 3.x, у мене є список рядків, для яких я хотів би виконати натуральний алфавітний сорт.

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

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

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

І ось "відсортована" версія вищевказаного списку (що я маю):

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

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


13
Визначення природного сорту не є "порядком сортування файлів Windows".
Гленн Мейнард


Усі відповіді на цьому веб-сайті дадуть неправильні результати, якщо ви хочете сортувати «Windows-Explorer» у кількох випадках, наприклад, сортування !1, 1, !a, a. Здається, єдиним способом сортування, як Windows, є використання самої StrCmpLogicalW функції Windows , оскільки, здається, ніхто не повторно реалізував цю функцію (джерело буде вдячно). Рішення: stackoverflow.com/a/48030307/2441026
user136036

Відповіді:


235

Для цього на 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']

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)? Це для будь-якого підвищення продуктивності чи просто бути більш пітонічним?
jperelli

12
@jperelli Я думаю, що сходи змінили б оригінальний список у виклику. Але, швидше за все, абонент хоче чергову неглибоку копію списку.
huggie

3
Тільки для запису, це не може обробляти всі входи: str / int розбивається на лінійку, інакше ви створите порівняння типу ["foo", 0] <[0, "foo"] для введення ["foo0 "," 0foo "], що викликає TypeError.
user19087

4
@ user19087: насправді це працює, тому що re.split('([0-9]+)', '0foo')повертається ['', '0', 'foo']. Через це рядки завжди будуть на парних індексах та цілих числах непарних індексів у масиві.
Флоріан Куше

Для тих, хто цікавиться продуктивністю, це помітно повільніше, ніж рідний питон. тобто на 25 -50 разів повільніше. І якщо ви хочете завжди надійно сортувати [elm1, elm2, Elm2, elm2] як [elm1, Elm2, elm2, elm2] (кришки перед нижньою), тоді ви можете просто зателефонувати natural_sort (сортується (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
повторно модуль збирає та кешує регексети автоматично, тому немає необхідності в попередньому компілюванні
wim

1
@wim: він кешує останні X звичаї, тому технічно можливо використовувати регекси X + 5, а потім робити природне сортування знову і знову, і в цей момент це не буде кешовано. але, мабуть, незначно в довгостроковій перспективі
Клавдіу

Я цього не робив, але, можливо, причина полягала в тому, що він не може обробляти кортежі, як звичайний пітон.
The Unfun Cat

1
Згадані в @Claudiu X звичаї становлять 100 на Python 2.7 та 512 на Python 3.4. А також зауважте, що при досягненні межі кеш повністю очищається (тому викидається не лише найстаріший).
Zitrax

@Zitrax Чому / Як має сенс повністю очистити кеш?
Джошуа

19

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

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

більш простим способом зробити це було б визначити natural_sort_key, а потім, сортуючи список, ви могли б зав'язати свої ключі, наприклад:list.sort(key=lambda el: natural_sort_key(el['name']))
Клавді

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
А тепер про щось більш * елегантне (пітонічне) - просто на дотик

Існує багато реалізацій там, і хоча деякі з них наблизилися, жодна не дуже захоплювала елегантність, яку надає сучасний пітон.

  • Випробувано за допомогою пітона (3.5.1)
  • Додано додатковий список, який демонструє, що він працює, коли числа є середнім рядком
  • Однак не перевіряв, я припускаю, що якби ваш список був значним, було б ефективніше заздалегідь скласти регулярний вираз.
    • Я впевнений, що хтось мене виправить, якщо це помилкове припущення

Швидкий
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
    • вам потрібно буде диференціювати імпорт

Натхнення від


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

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

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, і оскільки існує багато більше цифр унікоду, ніж [0-9]створення такого сортування було б однаково важким. Якщо ви хочете порівняння, що знає місце, підготуйте свої рядки locale.strxfrmза допомогою сортування Python, ЯК ДО .


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=натуральним сортом:

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

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

Я залишаю читачеві реалізацію нечутливого до регістру :-)


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)

Я також зробив « Гіст»


(-1) ця відповідь не приносить нічого нового порівняно з Марком (будь-який літер може PEP8-ify якийсь код). А може 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

Подяка :

Бульбашка Сортувати домашнє завдання

Як читати рядок по одній букві одночасно в python


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

4
Ваша реалізація вирішує лише задачу чисел. Реалізація завершується невдачею, якщо в рядках немає чисел. Спробуйте, наприклад, ['silent', 'ghost'] (список індексу поза діапазоном).
snakile

2
@snaklie: ваше запитання не дає пристойного прикладу. Ви не пояснили, що намагаєтесь зробити, і не оновили своє запитання цією новою інформацією. Ви не опублікували нічого, що ви пробували, тому, будь ласка, не будьте таким зневажливим щодо моєї спроби телепатії.
SilentGhost

5
@SilentGhost: По-перше, я дав вам нагороду, тому що вважаю, що ваша відповідь корисна (навіть якщо це не вирішує мою проблему). По-друге, я не можу висвітлити всі можливі випадки прикладами. Я думаю, що я дав досить чітке визначення природному сорту. Я не думаю, що давати складний приклад чи довге визначення такому простому поняттю. Ви можете відредагувати моє запитання, якщо ви можете придумати кращу постановку проблеми.
snakile

1
@SilentGhost: Я б хотів обробляти такі рядки так само, як Windows поводиться з такими іменами файлів, коли сортує файли за іменем (ігнорує випадки тощо). Мені це здається зрозумілим, але все, що я кажу, здається мені зрозумілим, тому я не суджу, чи це ясно, чи ні.
snakile

1
@snakile ви ніде не знайшли близького визначення природного пошуку. Це було б досить важко зробити і вимагало б багато деталей. Якщо ви хочете, щоб порядок сортування, використовуваний Windows Explorer, чи знаєте ви, що існує простий дзвінок api, який забезпечує це?
Девід Геффернан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.