37. 組み込み関数と便利ツール
Python には、どのモジュールも import せずに常に利用できる、豊富な組み込み関数が用意されています。これらの関数は日常的な Python プログラミングの土台となり、データ、シーケンス、コレクションを効率よく扱えるようにしてくれます。この章では、Python の特に便利な組み込みツールを見ていき、それらを活用してよりクリーンで表現力の高いコードを書く方法を学びます。
Python の型システムを理解する
特定の組み込み関数に入る前に、Python がデータ型をどのように整理しているかを理解しておくと役立ちます。この知識があれば、どの操作がどの型で動くのかを予測でき、エラーが発生したときにエラーメッセージも理解しやすくなります。
Python のデータ型は、次の 2 つの相補的な観点から理解できます。
型階層: 型同士の関係
これは、Python が「それが何であるか」に基づいて型をファミリーとしてどのように整理しているかを示しています。
能力ベースの見方: 型にできること
組み込み関数にとってより重要なのは、型が「何ができるか」です。
主要な能力:
- Iterable:
forループで使える →sum()、any()、all()、sorted()が使える - Collection:
len()を持つ Iterable →len()とin演算子が使える - Sequence: インデックス参照ができる Collection →
[index]とスライス[start:end]をサポート
なぜこれが重要なのか
組み込み関数は特定の能力を必要とします。
| 関数 | 必要とするもの | 対応する型 |
|---|---|---|
len() | Collection | str, list, dict, set, tuple |
sum() | 数値の Iterable | list, tuple, set, range, generator |
sorted() | Iterable | str, list, dict, set, tuple |
[index] | Sequence | str, list, tuple, range |
これらのカテゴリを理解しておくと、次のことができるようになります。
- どの関数がどの型で動くかを予測する
- 「object is not iterable」のようなエラーメッセージを理解する
- インデックス参照(
[0])ができる場面と、ループ(for)しかできない場面を把握する
37.1) よく使う組み込み関数(len, sum, min, max, abs, round)
Python で最も頻繁に使われる組み込み関数は、ループや複雑なロジックを書かずに、データに対して一般的な操作を行うのに役立ちます。これらの関数は最適化されており、読みやすく、Python らしい(Pythonic)コードの土台を形成します。
37.1.1) len() で長さを測る
len() 関数は、コレクション内の要素数を返します。文字列、リスト、タプル、辞書、集合、その他のコレクション型で動作します。
# 文字列中の文字数を数える
message = "Hello, World!"
print(len(message)) # Output: 13
# リストの要素数を数える
scores = [85, 92, 78, 90, 88]
print(len(scores)) # Output: 5
# 辞書のキーと値のペア数を数える
student = {"name": "Bob", "age": 21, "major": "CS"}
print(len(student)) # Output: 3
# set 内のユニーク要素数を数える
unique_ids = {101, 102, 103, 101, 102} # 重複は削除される
print(len(unique_ids)) # Output: 3len() 関数は、処理を始める前にデータのサイズを知りたい場合に特に便利です。
# サイズに応じてデータを処理する
data = [12, 45, 23, 67, 89, 34]
if len(data) < 5:
print("Not enough data for analysis")
else:
print(f"Analyzing {len(data)} data points") # Output: Analyzing 6 data points
average = sum(data) / len(data)
print(f"Average: {average}") # Output: Average: 45.037.1.2) sum() で合計を計算する
sum() 関数は、Iterable 内の数値をすべて足し合わせます。値を蓄積するループを書くよりもずっとすっきりします。
# 数値リストの合計
prices = [19.99, 24.50, 15.75, 32.00]
total = sum(prices)
print(f"Total: ${total}") # Output: Total: $92.24
# タプルの合計
daily_steps = (8500, 10200, 7800, 9500, 11000)
weekly_total = sum(daily_steps)
print(f"Total steps this week: {weekly_total}") # Output: Total steps this week: 47000
# range の合計
total_1_to_100 = sum(range(1, 101))
print(total_1_to_100) # Output: 5050sum() と len() を組み合わせて平均を計算する実用例です。
# テストの平均点を計算する
test_scores = [88, 92, 79, 85, 90, 87]
total_score = sum(test_scores)
num_tests = len(test_scores)
average_score = total_score / num_tests
print(f"Average score: {average_score:.1f}") # Output: Average score: 86.8重要な制限: sum() が扱えるのは数値だけです。文字列の連結やリストの結合には使えません。
# これは TypeError を発生させます
words = ["Hello", " ", "World"]
# sentence = sum(words) # TypeError: unsupported operand type(s)37.1.3) min() と max() で最小・最大を見つける
min() と max() 関数は、Iterable の中から最小値・最大値を見つけます。数値、文字列、そして比較可能な任意のオブジェクトで動作します。
# 最小・最大の数値を見つける
temperatures = [72, 68, 75, 70, 73, 69]
coldest = min(temperatures)
warmest = max(temperatures)
print(f"Temperature range: {coldest}°F to {warmest}°F")
# Output: Temperature range: 68°F to 75°F
# 最小・最大の文字列を見つける(アルファベット順)
names = ["Zoe", "Alice", "Bob", "Charlie"]
first_alphabetically = min(names)
last_alphabetically = max(names)
print(f"First: {first_alphabetically}, Last: {last_alphabetically}")
# Output: First: Alice, Last: Zoeコレクションではなく、複数の引数を直接渡すこともできます。
# 個別の値を比較する
lowest = min(45, 23, 67, 12, 89)
highest = max(45, 23, 67, 12, 89)
print(f"Lowest: {lowest}, Highest: {highest}")
# Output: Lowest: 12, Highest: 89
# 特定の少数の値を比較するときに便利
price1 = 19.99
price2 = 24.50
price3 = 15.75
cheapest = min(price1, price2, price3)
print(f"Cheapest option: ${cheapest}") # Output: Cheapest option: $15.7537.1.4) abs() で絶対値を取得する
abs() 関数は数値の絶対値(ゼロからの距離)を返し、常に正になります。方向ではなく大きさだけが重要なときに役立ちます。
# 負の数の絶対値
print(abs(-42)) # Output: 42
print(abs(-3.14)) # Output: 3.14
# 正の数の絶対値(変化なし)
print(abs(42)) # Output: 42
print(abs(3.14)) # Output: 3.14
# ゼロの絶対値
print(abs(0)) # Output: 0よくある用途として、方向に関係なく差分だけを計算する場面があります。
# 気温変化を計算する(大きさのみ)
morning_temp = 65
evening_temp = 72
temperature_change = abs(evening_temp - morning_temp)
print(f"Temperature changed by {temperature_change}°F")
# Output: Temperature changed by 7°F37.1.5) round() で数値を丸める
round() 関数は、数値を指定した小数点以下桁数で丸めます。第 2 引数を省略すると、最も近い整数に丸めます。
# 最も近い整数に丸める
print(round(3.7)) # Output: 4
print(round(3.2)) # Output: 3
print(round(3.5)) # Output: 4
print(round(4.5)) # Output: 4 (最も近い偶数に丸める)
# 小数点以下の桁数を指定して丸める
price = 19.876
print(round(price, 2)) # Output: 19.88 (小数第 2 位まで)
print(round(price, 1)) # Output: 19.9 (小数第 1 位まで)
# 負の小数桁で丸める(10、100 などの単位に丸める)
population = 1234567
print(round(population, -3)) # Output: 1235000 (千の位)
print(round(population, -4)) # Output: 1230000 (万の位)ちょうど中間の値についての注意: ちょうど 2 つの整数の中間にある数を丸める場合、Python には特別なルールがあります。たとえば 2.5 は 2 と 3 のちょうど中間です。3 に切り上げられると期待するかもしれませんが、Python は近い方の偶数に丸めます。この場合は 2 です。
これは「banker's rounding」または「round half to even」と呼ばれます。IEEE 754 標準の一部であり、多数の丸め処理を行ったときの偏りを減らすのに役立ちます。
# ちょうど中間の値は最も近い偶数に丸められる
print(round(0.5)) # Output: 0 (0 は偶数)
print(round(1.5)) # Output: 2 (2 は偶数)
print(round(2.5)) # Output: 2 (2 は偶数)
print(round(3.5)) # Output: 4 (4 は偶数)
print(round(4.5)) # Output: 4 (4 は偶数)37.2) enumerate() でシーケンスを列挙する
シーケンスをループするとき、要素とその位置の両方が必要になることがよくあります。enumerate() 関数はその両方を提供してくれるため、手動でカウンタ変数を管理する必要がなくなります。
37.2.1) 手動カウンタの問題点
enumerate() を学ぶ前は、位置を追跡するためにカウンタ変数を使うことがよくあります。
# 手動カウンタ方式(動くが理想的ではない)
fruits = ["apple", "banana", "cherry", "date"]
index = 0
for fruit in fruits:
print(f"{index}: {fruit}")
index += 1
# Output:
# 0: apple
# 1: banana
# 2: cherry
# 3: dateこの方法にはいくつかの欠点があります。
- 管理が必要な変数が 1 つ増える(
index) - カウンタのインクリメントを忘れやすい
37.2.2) enumerate() で位置と値を取得する
enumerate() 関数はこの問題をエレガントに解決します。Iterable を受け取り、(index, element)のペアを返します。
# enumerate() を使う - よりすっきりして Pythonic
fruits = ["apple", "banana", "cherry", "date"]
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")
# Output:
# 0: apple
# 1: banana
# 2: cherry
# 3: datefor index, fruit in enumerate(fruits) という構文は タプルのアンパック(tuple unpacking)(第 15 章で学びました)を使っています。各反復で enumerate() は (0, "apple") のようなタプルを提供し、それが変数 index と fruit にアンパックされます。
enumerate() が実際に生成するものは次の通りです。
# enumerate の出力を直接確認する
fruits = ["apple", "banana", "cherry"]
enumerated = list(enumerate(fruits))
print(enumerated)
# Output: [(0, 'apple'), (1, 'banana'), (2, 'cherry')]37.2.3) 列挙を別の数から始める
デフォルトでは、enumerate() は 0 から数え始めます。start パラメータで開始番号を指定できます。
# 1 から数え始める(表示に便利)
tasks = ["Write code", "Test code", "Deploy code"]
for number, task in enumerate(tasks, start=1):
print(f"Step {number}: {task}")
# Output:
# Step 1: Write code
# Step 2: Test code
# Step 3: Deploy codeこれは、通常ユーザーが 1 から数えることを期待する、番号付きリストを表示するときに特に便利です。
# 番号付きオプションのメニュー
menu_items = ["New Game", "Load Game", "Settings", "Quit"]
print("Main Menu:")
for number, item in enumerate(menu_items, start=1):
print(f"{number}. {item}")
# Output:
# Main Menu:
# 1. New Game
# 2. Load Game
# 3. Settings
# 4. Quit37.2.4) 文字列やその他の Iterable での enumerate()
enumerate() 関数は、リストだけでなく任意の Iterable で動作します。
# 文字列中の文字を列挙する
word = "Python"
for position, letter in enumerate(word):
print(f"Letter {position}: {letter}")
# Output:
# Letter 0: P
# Letter 1: y
# Letter 2: t
# Letter 3: h
# Letter 4: o
# Letter 5: n
# タプルを列挙する
coordinates = (10, 20, 30, 40)
for index, value in enumerate(coordinates):
print(f"Coordinate {index}: {value}")
# Output:
# Coordinate 0: 10
# Coordinate 1: 20
# Coordinate 2: 30
# Coordinate 3: 40enumerate() 関数を使うとコードの可読性が上がり、エラーも起きにくくなります。ループで位置と値の両方が必要なときは、手動でカウンタを管理するのではなく、enumerate() を使うようにしましょう。
37.3) zip() でシーケンスを結合する
zip() 関数は、複数の Iterable を要素ごとに結合し、対応する要素のペア(またはタプル)を作ります。別々のシーケンスにある関連データを同時に処理する必要がある場合に非常に便利です。
37.3.1) zip() の仕組みを理解する
zip() 関数は 2 つ以上の Iterable を受け取り、各入力 Iterable から 1 つずつ要素を取ったタプルのイテレータを返します。
# 2 つのリストを結合する
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
combined = list(zip(names, ages))
print(combined)
# Output: [('Alice', 25), ('Bob', 30), ('Charlie', 35)]「zip」という名前は衣服のジッパーから来ています。2 つの別々の側を、要素ごとに 1 つに結合していくイメージです。
zip() が要素をどのようにペアにするかを視覚的に表すと次のようになります。
37.3.2) ループで zip() を使う
zip() の最も一般的な使い方は、複数のシーケンスを同時に反復する必要がある for ループです。
# 並行データを処理する
students = ["Alice", "Bob", "Charlie", "Diana"]
scores = [92, 85, 88, 95]
for student, score in zip(students, scores):
print(f"{student} scored {score}")
# Output:
# Alice scored 92
# Bob scored 85
# Charlie scored 88
# Diana scored 95これは、インデックスを使うよりもずっとすっきりします。
# zip() なし - より複雑でミスしやすい
students = ["Alice", "Bob", "Charlie", "Diana"]
scores = [92, 85, 88, 95]
for i in range(len(students)):
print(f"{students[i]} scored {scores[i]}")
# Same output, but more code and potential for index errors37.3.3) 長さの異なるシーケンスを扱う
入力シーケンスの長さが異なる場合、zip() は最も短いシーケンスが尽きた時点で停止します。
# 長さが異なるシーケンス
names = ["Alice", "Bob", "Charlie", "Diana"]
ages = [25, 30] # 年齢は 2 つだけ
for name, age in zip(names, ages):
print(f"{name} is {age} years old")
# Output:
# Alice is 25 years old
# Bob is 30 years old
# (Charlie and Diana are not processed)この挙動はエラーを防ぎますが、注意しないと黙ってデータが欠ける可能性があります。シーケンスの長さが想定通りかどうかは常に確認しましょう。
# 長さの不一致をチェックする
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30]
if len(names) != len(ages):
print(f"Warning: {len(names)} names but {len(ages)} ages")
print("Only processing the first", min(len(names), len(ages)), "entries")
# Output: Warning: 3 names but 2 ages
# Output: Only processing the first 2 entries
# zip() で続行 - 最短のところで止まる
for name, age in zip(names, ages):
print(f"{name} is {age} years old")37.3.4) 2 つ以上のシーケンスを zip する
zip() 関数は任意の数の Iterable を結合できます。
# 3 つのシーケンスを結合する
products = ["Laptop", "Mouse", "Keyboard"]
prices = [999.99, 24.99, 79.99]
quantities = [5, 20, 15]
print("Inventory Report:")
for product, price, quantity in zip(products, prices, quantities):
total_value = price * quantity
print(f"{product}: ${price} × {quantity} = ${total_value:.2f}")
# Output:
# Inventory Report:
# Laptop: $999.99 × 5 = $4999.95
# Mouse: $24.99 × 20 = $499.80
# Keyboard: $79.99 × 15 = $1199.8537.3.5) zip() で辞書を作る
強力なパターンとして、zip() を使ってキーと値の別々のシーケンスから辞書を作れます。
# 2 つのリストから辞書を作る
keys = ["name", "age", "city"]
values = ["Alice", 25, "Boston"]
person = dict(zip(keys, values))
print(person)
# Output: {'name': 'Alice', 'age': 25, 'city': 'Boston'}37.4) any() と all() による真偽の集約
any() と all() 関数は、Iterable 全体にわたって条件をテストし、単一の真偽値(boolean)を返します。複数条件に基づくバリデーションや意思決定において強力なツールです。
37.4.1) any() を理解する: 少なくとも 1 つが True なら True
any() 関数は、Iterable 内に少なくとも 1 つ truthy(True と評価される)な要素があれば True を返します。すべて falsy なら False を返します。
# any() の基本例
print(any([True, False, False])) # Output: True (at least one True)
print(any([False, False, False])) # Output: False (all False)
print(any([False, True, True])) # Output: True (multiple True values)
# 空の Iterable
print(any([])) # Output: False (no elements to be True)any() 関数は Python の truthiness ルール(第 7 章で学びました)を使います。ゼロでない数、空でない文字列、空でないコレクションは truthy です。
# さまざまな truthy/falsy 値での any()
print(any([0, 0, 1])) # Output: True (1 is truthy)
print(any([0, 0, 0])) # Output: False (all zeros are falsy)
print(any(["", "", "text"])) # Output: True ("text" is truthy)
print(any(["", "", ""])) # Output: False (empty strings are falsy)37.4.2) any() の実用例
例: いずれかの条件が満たされているかを確認する
# いずれかの点数が不合格(60 未満)かを確認する
scores = [75, 82, 55, 90, 88]
has_failing_grade = any(score < 60 for score in scores)
if has_failing_grade:
print("Warning: At least one failing grade")
# Output: Warning: At least one failing grade
else:
print("All grades are passing")37.4.3) all() を理解する: すべてが True の場合のみ True
all() 関数は、Iterable 内のすべての要素が truthy のときにのみ True を返します。1 つでも falsy な要素があれば False を返します。
# all() の基本例
print(all([True, True, True])) # Output: True (all True)
print(all([True, False, True])) # Output: False (one False)
print(all([True, True, False])) # Output: False (one False)
# 空の Iterable
print(all([])) # Output: True (vacuous truth - no False elements)空の Iterable に対する挙動は意外に思えるかもしれませんが、all([]) は True を返します。これは vacuous truth(空虚真)と呼ばれ、「すべての要素が True である」という命題は、それに反する要素が存在しない場合に技術的には真である、という考え方です。
# さまざまな truthy/falsy 値での all()
print(all([1, 2, 3])) # Output: True (all non-zero)
print(all([1, 0, 3])) # Output: False (0 is falsy)
print(all(["a", "b", "c"])) # Output: True (all non-empty)
print(all(["a", "", "c"])) # Output: False (empty string is falsy)37.4.4) all() の実用例
例: すべての条件が満たされていることを検証する
# すべての点数が合格(60 以上)かを確認する
scores = [75, 82, 68, 90, 88]
all_passing = all(score >= 60 for score in scores)
if all_passing:
print("Congratulations! All grades are passing")
# Output: Congratulations! All grades are passing
else:
print("Some grades need improvement")37.4.5) any() と all() の短絡評価
any() と all() の両方は 短絡評価(short-circuit evaluation)(第 9 章で学びました)を使います。結果が確定した時点でチェックを止めます。
# 呼び出されたときに表示する関数(実行を示すため)
def is_positive(n):
print(f"Checking {n}")
return n > 0
# any() は最初の True で止まる
print("Testing any():")
numbers = [0, 0, 1, 2, 3]
result = any(is_positive(n) for n in numbers)
# Output:
# Testing any():
# Checking 0
# Checking 0
# Checking 1
# (Stops here - doesn't check 2 or 3)
print(f"Result: {result}") # Output: Result: True
print("\nTesting all():")
numbers = [1, 2, 0, 3, 4]
result = all(is_positive(n) for n in numbers)
# Output:
# Testing all():
# Checking 1
# Checking 2
# Checking 0
# (Stops here - doesn't check 3 or 4)
print(f"Result: {result}") # Output: Result: Falseこのため any() と all() は効率的です。結果が確定した後の要素を無駄にチェックしません。
37.5) sorted() とカスタムキーによるソート
sorted() 関数は、任意の Iterable から新しいソート済みリストを作ります。.sort() メソッド(リストにしか使えず、インプレースで変更します)とは異なり、sorted() は任意の Iterable で動作し、常に新しいリストを返します。
37.5.1) sorted() の基本ソート
sorted() 関数はデフォルトで要素を昇順に並べます。
# 数値をソートする
numbers = [42, 17, 93, 8, 55]
sorted_numbers = sorted(numbers)
print(sorted_numbers) # Output: [8, 17, 42, 55, 93]
# 元のリストは変更されない
print(numbers) # Output: [42, 17, 93, 8, 55]
# 文字列をソートする(アルファベット順)
names = ["Charlie", "Alice", "Bob", "Diana"]
sorted_names = sorted(names)
print(sorted_names) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']sorted() 関数はリストに限らず、任意の Iterable で動作します。
# タプルをソートする(リストを返す)
coordinates = (5, 2, 8, 1, 9)
sorted_coords = sorted(coordinates)
print(sorted_coords) # Output: [1, 2, 5, 8, 9]
# 文字列をソートする(文字のリストを返す)
word = "python"
sorted_letters = sorted(word)
print(sorted_letters) # Output: ['h', 'n', 'o', 'p', 't', 'y']
# set をソートする(ソート済みリストを返す)
unique_numbers = {5, 8, 2, 1}
sorted_unique = sorted(unique_numbers)
print(sorted_unique) # Output: [1, 2, 5, 8]37.5.2) 逆順ソート
reverse=True パラメータを使うと降順にソートできます。
# 数値を降順にする
scores = [85, 92, 78, 95, 88]
highest_first = sorted(scores, reverse=True)
print(highest_first) # Output: [95, 92, 88, 85, 78]
# 文字列を降順にする(逆アルファベット順)
names = ["Charlie", "Alice", "Bob", "Diana"]
reverse_alpha = sorted(names, reverse=True)
print(reverse_alpha) # Output: ['Diana', 'Charlie', 'Bob', 'Alice']37.5.3) key パラメータを理解する
key パラメータこそが、sorted() を本当に強力にする要素です。ソート時に Python が要素を比較する方法を変えられます。
key パラメータとは?
key パラメータは 関数(function) を受け取ります。Python は各要素に対してこの関数を呼び出して「比較キー」を取り出し、元の要素ではなくそのキーを基準にソートします。
手順で見る仕組み:
- Python が各要素に key 関数を呼び出す
- Python がすべてのキーを集める
- Python がそれらのキーを比較してソートする
- Python が新しい順序で元の要素を返す
# 例: 長さでソートする
words = ["python", "is", "awesome"]
# Step 1: Python は各単語に len() を呼び出す
# len("python") → 6
# len("is") → 2
# len("awesome") → 7
# Step 2: Python はこれらのキーを持つ: [6, 2, 7]
# Step 3: Python はキーをソートする: [2, 6, 7]
# Step 4: Python はその順序で words を返す: ["is", "python", "awesome"]
result = sorted(words, key=len)
print(result) # Output: ['is', 'python', 'awesome']key 関数を可視化する:
キー関数には何が使える?
キー関数は次の条件を満たす必要があります。
- 引数を 1 つ受け取る(ソート対象の要素)
- Python が比較できる値(数値、文字列、タプルなど)を返す
# 組み込み関数は key として非常に便利
sorted(numbers, key=abs) # 絶対値でソート
sorted(words, key=len) # 長さでソート
sorted(names, key=str.lower) # 大文字小文字を区別せずにソート
# 自作関数
def first_letter(word):
return word[0]
sorted(words, key=first_letter) # 先頭文字でソート
# lambda 関数(第 23 章)
sorted(words, key=lambda w: w[-1]) # 最後の文字でソート重要: key 関数は要素ごとに 1 回だけ呼ばれる
# key 関数がいつ呼ばれるかを示す
def show_key(word):
print(f"Getting key for: {word}")
return len(word)
words = ["cat", "elephant", "dog"]
result = sorted(words, key=show_key)
# Output:
# Getting key for: cat
# Getting key for: elephant
# Getting key for: dog
print(result) # Output: ['cat', 'dog', 'elephant']重要: key 関数は要素ごとに 1 回だけ呼ばれる
show_key が各単語につきちょうど 1 回だけ呼ばれており、比較のたびに繰り返し呼ばれているわけではない点に注目してください。Python は効率的で、まずすべてのキーを取り出してキャッシュし、そのキャッシュされたキーを使ってソートします。
key は「何を比較すべきか?」に答えるものと考える
key=len→ 「長さで比較する」key=abs→ 「絶対値で比較する」key=str.lower→ 「すべて小文字だとみなして比較する」key=lambda x: x[1]→ 「2 番目の要素で比較する」
key パラメータを使えば、要素の任意の性質でソートできるため、sorted() は非常に汎用的になります。
37.5.4) 組み込み関数を key として使うソート
Python の組み込み関数は優れた key 関数になります。
# 絶対値でソートする
numbers = [-5, 2, -8, 1, -3, 7]
sorted_by_magnitude = sorted(numbers, key=abs)
print(sorted_by_magnitude) # Output: [1, 2, -3, -5, 7, -8]
# 大文字小文字を区別せずに文字列をソートする
names = ["alice", "Bob", "CHARLIE", "diana"]
sorted_case_insensitive = sorted(names, key=str.lower)
print(sorted_case_insensitive) # Output: ['alice', 'Bob', 'CHARLIE', 'diana']37.5.5) 複雑なデータ構造をソートする
タプルのリストやリストのリストをソートする場合、インデックスを使ってどの要素でソートするかを指定できます。
# タプルを第 2 要素でソートする
students = [
("Alice", 92),
("Bob", 85),
("Charlie", 88),
("Diana", 95)
]
# 点数(第 2 要素)でソート
by_score = sorted(students, key=lambda student: student[1])
print(by_score)
# Output: [('Bob', 85), ('Charlie', 88), ('Alice', 92), ('Diana', 95)]
# 点数を降順でソート
by_score_desc = sorted(students, key=lambda student: student[1], reverse=True)
print(by_score_desc)
# Output: [('Diana', 95), ('Alice', 92), ('Charlie', 88), ('Bob', 85)]Note: ここでは lambda(第 23 章で学びました)を使っています。lambda は小さな無名関数です。式 lambda student: student[1] は、student タプルを受け取り、その第 2 要素(点数)を返す関数を作ります。
37.5.6) 多段(複数基準)ソート
キー関数からタプルを返すことで、複数条件でソートできます。Python はタプルを左から右へ、要素ごとに比較します。
タプル比較の仕組み:
Python が 2 つのタプルを比較するとき、次のルールに従います。
- 最初の要素を比較し、異なればそこで比較は終了します。
- 最初の要素が等しければ、2 番目の要素を比較します。
- 2 番目の要素が等しければ、3 番目の要素を比較します。
- 差が見つかるか、要素が尽きるまで続けます。
# タプル比較の例
print((1, 'a') < (2, 'z')) # Output: True (1 < 2, so True immediately)
print((1, 'z') < (1, 'a')) # Output: False (1 == 1, so compare 'z' < 'a')
print((1, 'a') < (1, 'a')) # Output: False (both tuples are equal)
print((1, 2, 9) < (1, 3, 1)) # Output: True (1 == 1, then 2 < 3)この性質により、タプルは多段ソートに最適です。Python が「第 1 基準、次に第 2 基準、次に第 3 基準」という比較ロジックを自動的に処理してくれます。
# 複数基準でソートする
students = [
("Alice", "Smith", 92),
("Bob", "Jones", 85),
("Alice", "Brown", 88),
("Charlie", "Smith", 85)
]
# 名、次に姓でソート
by_name = sorted(students, key=lambda s: (s[0], s[1]))
print("By name:")
for student in by_name:
print(f" {student}")
# Output:
# By name:
# ('Alice', 'Brown', 88)
# ('Alice', 'Smith', 92)
# ('Bob', 'Jones', 85)
# ('Charlie', 'Smith', 85)
# 点数を降順、その後に名前を昇順でソート
by_score_then_name = sorted(students, key=lambda s: (-s[2], s[0]))
print("\nBy score (high to low), then name:")
for student in by_score_then_name:
print(f" {student}")
# Output:
# By score (high to low), then name:
# ('Alice', 'Smith', 92)
# ('Alice', 'Brown', 88)
# ('Bob', 'Jones', 85)
# ('Charlie', 'Smith', 85)Note: ある基準を降順、別の基準を昇順にするには、数値を否定します(-s[2])。否定すると数値の並び順が逆になるためです。上の例では、-s[2] で点数を高い順にし、s[0] で名前を A から Z に並べています。
37.5.7) 複雑なキーのためにヘルパー関数を使う
ソートのロジックが複雑になってきたら、ヘルパー関数を定義するとコードの可読性と保守性が上がります。そのうえで、そのヘルパー関数を key 関数の中で使えます。
例: 拡張子でファイルをソートする
たとえば、拡張子(.csv、.jpg、.pdf など)ごとにファイルをグループ化し、各グループの中ではファイル名のアルファベット順に並べたいとします。キー関数は拡張子を取り出す必要があり、文字列の加工が必要になります。
# 拡張子でソートし、その後に名前でソートする
files = [
"report.pdf",
"data.csv",
"image.jpg",
"notes.txt",
"backup.csv",
"photo.jpg"
]
# ソート用に拡張子を取り出す
def get_extension(filename):
"""ファイル名から拡張子を抽出する。"""
return filename.split(".")[-1] # "." で分割し、最後の部分を取得する
# key の中でヘルパー関数を使う
sorted_files = sorted(files, key=lambda f: (get_extension(f), f))
print("Files sorted by extension, then name:")
for file in sorted_files:
print(f" {file}")
# Output:
# Files sorted by extension, then name:
# backup.csv # csv files first (alphabetically)
# data.csv # csv files first (alphabetically)
# image.jpg # jpg files next
# photo.jpg # jpg files next
# report.pdf # pdf files next
# notes.txt # txt files last仕組み:
- キー関数
lambda f: (get_extension(f), f)は、各ファイル名ごとにタプルを返します - "report.pdf" なら
("pdf", "report.pdf")を返します - "data.csv" なら
("csv", "data.csv")を返します - Python はタプルの第 1 要素(拡張子)でソートし、次に第 2 要素(ファイル名全体)でソートします
- これにより拡張子でグループ化され、各グループ内でアルファベット順に並びます
なぜヘルパー関数を使うのか?
読みやすさを比べてみましょう。
# ヘルパー関数なし - 理解しにくい
sorted_files = sorted(files, key=lambda f: (f.split(".")[-1], f))
# ヘルパー関数あり - 意図が明確
sorted_files = sorted(files, key=lambda f: (get_extension(f), f))ヘルパー関数を使うとコードが自己文書化されます。get_extension(f) を見れば何をしているかがすぐ分かりますが、f.split(".")[-1] は頭の中で解釈する必要があります。
37.5.8) sorted() と .sort(): 使い分け
Python には 2 つのソート方法があります。
sorted()- 新しいソート済みリストを返す関数.sort()- リストをインプレースでソートするメソッド
# sorted() - 新しいリストを作成し、元は変更しない
numbers = [3, 1, 4, 1, 5]
sorted_numbers = sorted(numbers)
print(f"Original: {numbers}") # Output: Original: [3, 1, 4, 1, 5]
print(f"Sorted: {sorted_numbers}") # Output: Sorted: [1, 1, 3, 4, 5]
# .sort() - リストをインプレースで変更し、None を返す
numbers = [3, 1, 4, 1, 5]
result = numbers.sort()
print(f"Modified: {numbers}") # Output: Modified: [1, 1, 3, 4, 5]
print(f"Return value: {result}") # Output: Return value: Nonesorted() を使う場面:
- 元の順序を保持したい
- リスト以外(タプル、文字列、set など)をソートしたい
- ソートして代入する処理を 1 つの式で書きたい
.sort() を使う場面:
- リストがあり、元の順序が不要
- メモリを節約したい(新しいリストを作らない)
- 大きなリストを効率重視でインプレースにソートしたい
sorted() 関数は Python の中でも特に汎用性の高いツールの 1 つです。key パラメータと組み合わせれば、単純な数値の並び替えから、入れ子になったデータ構造を複数基準でソートするような複雑な要件まで、事実上あらゆるソートを扱えます。
この章では、Python に欠かせない組み込み関数とツールを身につけました。学んだ内容は次の通りです。
- Python の型階層を理解し、どの操作がどの型で動くかを予測する
- よくある操作のために
len()、sum()、min()、max()、abs()、round()のような基本関数を使う enumerate()で位置情報を伴って反復処理するzip()で並行するシーケンスを同時に処理するany()とall()でコレクション全体にわたって判断するsorted()とカスタム key 関数で柔軟にデータをソートする
これらのツールは、Python らしい(idiomatic)コードの土台になります。効率的で読みやすく、エッジケースも正しく扱えます。プログラミングを続けていくと、これらの関数を常に使いたくなるはずです。これらは Python コードをエレガントで表現力豊かにするための構成要素です。
次の章ではデコレータ(decorators)を扱います。デコレータを使うと、第 23 章で学んだ第一級関数(first-class function)の概念を土台として、関数の振る舞いを強力に変更・拡張できます。