Pythonでゼロ埋めなしの数字の文字列リストをソート

Modified: | Tags: Python, リスト, 文字列

Pythonでリストなどを昇順・降順にソートする(並べ替える)にはsort()メソッドやsorted()関数を使う。

ここでは、ゼロ埋めされていない数字の文字列のリストをソートする方法について説明する。

sort()とsorted()

sort()はリスト型のメソッドで、元のリスト自体がソートされる。

l = [10, 1, 5]

l.sort()
print(l)
# [1, 5, 10]

sorted()は組み込み関数で、ソートされた新たなリストが生成される。元のリストは変更されない。

l = [10, 1, 5]

print(sorted(l))
# [1, 5, 10]

print(l)
# [10, 1, 5]

デフォルトは昇順。降順にしたい場合は引数reverseTrueとする。例はsorted()だが、sort()でも同様。

print(sorted(l, reverse=True))
# [10, 5, 1]

タプルや文字列に対するソートなど、より詳しくは以下の記事を参照。

ゼロ埋めされていない数字の文字列の注意点

ゼロ埋めされている数字の文字列のリストの場合、特に問題なくソートされる。なお、以降のサンプルコードではsorted()を使うが、sort()でも同様。

l = ['10', '01', '05']

print(sorted(l))
# ['01', '05', '10']

ゼロ埋めされていない数字の文字列のリストの場合、数値としての大小ではなく文字列を辞書の並びにソートするので、以下のような結果になってしまう。例えば'10''5'より小さいとみなされる。

l = ['10', '1', '5']

print(sorted(l))
# ['1', '10', '5']

引数keyにint()やfloat()を指定

sort()sorted()では引数keyに関数を指定することで、その関数を適用した結果に対してソートが行われる。

引数keyに文字列を数値に変換するint()float()を指定することで、数値の大小で並べ替えられる。

関数を引数に指定するときは()を書くとエラーになるので注意。

l = ['10', '1', '5']

print(sorted(l, key=int))
# ['1', '5', '10']

print(sorted(l, key=float))
# ['1', '5', '10']

整数の文字列はint()でもfloat()でも変換可能だが、小数に対してはfloat()を使う必要がある。

l = ['10.0', '1.0', '5.0']

print(sorted(l, key=float))
# ['1.0', '5.0', '10.0']

sort()でも同様に引数keyを指定できる。

l = ['10', '1', '5']

l.sort(key=int)
print(l)
# ['1', '5', '10']

これまでの結果からも分かるように、keyに指定した関数はあくまでもソートの比較のためだけに適用され、結果は元のまま。上の例では文字列のままで、整数int型や浮動小数点数float型になったりはしない。

int型やfloat型の結果がほしい場合は、リスト内包表記で変換したリストをソートすればよい。

l = ['10', '1', '5']

print([int(s) for s in l])
# [10, 1, 5]

print(sorted([int(s) for s in l]))
# [1, 5, 10]

正規表現で文字列中の数値を抽出

数字だけの文字列は引数keyint()float()を指定するだけでよいが、以下のように文字列中に数値が埋め込まれている場合は注意が必要。

l = ['file10.txt', 'file1.txt', 'file5.txt']

正規表現モジュールreを使って文字列中の数字部分を抽出してから数値に変換する。

文字列中の数値が一つだけの場合

search()matchオブジェクトを取得し、group()メソッドでマッチした部分を文字列として取り出す。

正規表現のパターンとして\d+を使う。\dは数字、+は1文字以上の繰り返しを表し、\d+は1文字以上の連続した数字にマッチする。

import re

s = 'file5.txt'

print(re.search(r'\d+', s).group())
# 5

ここではバックスラッシュ\をそのまま書けるようにraw文字列を使っている。

文字列が返されるので数値に変換する場合はint()float()を使う。

print(type(re.search(r'\d+', s).group()))
# <class 'str'>

print(type(int(re.search(r'\d+', s).group())))
# <class 'int'>

これを無名関数(ラムダ式)でsort()sorted()の引数keyに指定する。

l = ['file10.txt', 'file1.txt', 'file5.txt']

print(sorted(l))
# ['file1.txt', 'file10.txt', 'file5.txt']

print(sorted(l, key=lambda s: int(re.search(r'\d+', s).group())))
# ['file1.txt', 'file5.txt', 'file10.txt']

要素数が少ない場合はあまり気にしなくてもよいが、compile()で正規表現オブジェクトを生成して使用したほうが効率的。

p = re.compile(r'\d+')
print(sorted(l, key=lambda s: int(p.search(s).group())))
# ['file1.txt', 'file5.txt', 'file10.txt']

文字列中の数値が複数ある場合

search()が返すのは最初にマッチした部分のみ。

s = '100file5.txt'

print(re.search(r'\d+', s).group())
# 100

findall()はマッチするすべての部分をリストとして返す。

print(re.findall(r'\d+', s))
# ['100', '5']

print(re.findall(r'\d+', s)[1])
# 5

パターン中の部分を()で囲んでおくと、groups()メソッドで該当部分のみを取り出すことも可能。

例えば、file(\d+)パターンはfileXXXという文字列のXXX部分(数字)を抽出できる。該当部分が一つだけでもタプルを返すので注意。

print(re.search(r'file(\d+)', s).groups())
# ('5',)

print(re.search(r'file(\d+)', s).groups()[0])
# 5

(\d+)\.とするとXXX.という文字列のXXX部分(数字)を抽出できる。.にはバックスラッシュが必要。

print(re.search(r'(\d+)\.', s).groups()[0])
# 5

いずれの方法を使ってもよい。findall()はシンプルだが、数字部分の個数が要素によってバラバラだと使えないので注意。

l = ['100file10.txt', '100file1.txt', '100file5.txt']

print(sorted(l, key=lambda s: int(re.findall(r'\d+', s)[1])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

print(sorted(l, key=lambda s: int(re.search(r'file(\d+)', s).groups()[0])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

print(sorted(l, key=lambda s: int(re.search(r'(\d+)\.', s).groups()[0])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

コンパイルする場合も同様。

p = re.compile(r'file(\d+)')
print(sorted(l, key=lambda s: int(p.search(s).groups()[0])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

文字列中に数値がない要素もある場合

すべての要素の中に数値が含まれていれば問題ないが、そうでない場合はマッチしない場合のケアが必要。

これまでのようにするとエラーとなる。

l = ['file10.txt', 'file1.txt', 'file5.txt', 'file.txt']

# print(sorted(l, key=lambda s:int(re.search(r'\d+', s).group())))
# AttributeError: 'NoneType' object has no attribute 'group'

例えば、以下のような関数を定義する。第一引数に文字列、第二引数に正規表現オブジェクト、第三引数にマッチしない場合の返り値を指定する。

def extract_num(s, p, ret=0):
    search = p.search(s)
    if search:
        return int(search.groups()[0])
    else:
        return ret

結果は以下の通り。groups()を使っているのでパターンには()が必要。

p = re.compile(r'(\d+)')

print(extract_num('file10.txt', p))
# 10

print(extract_num('file.txt', p))
# 0

print(extract_num('file.txt', p, 100))
# 100

第三引数は省略可能。

この関数をsort()sorted()の引数keyに指定すればよい。

print(sorted(l, key=lambda s: extract_num(s, p)))
# ['file.txt', 'file1.txt', 'file5.txt', 'file10.txt']

print(sorted(l, key=lambda s: extract_num(s, p, float('inf'))))
# ['file1.txt', 'file5.txt', 'file10.txt', 'file.txt']

数値が含まれていない要素を昇順の最後に持っていきたい場合、適当に大きい数値を返り値としてもよいが、無限大infとしておくとどの値よりも大きくできる。

数値が複数含まれている場合は、それに応じた正規表現パターンを使う。

l = ['100file10.txt', '100file1.txt', '100file5.txt', '100file.txt']

p = re.compile(r'file(\d+)')
print(sorted(l, key=lambda s: extract_num(s, p)))
# ['100file.txt', '100file1.txt', '100file5.txt', '100file10.txt']

print(sorted(l, key=lambda s: extract_num(s, p, float('inf'))))
# ['100file1.txt', '100file5.txt', '100file10.txt', '100file.txt']

関連カテゴリー

関連記事