テストの点数のリストから、最高点と合計点を出したい。プログラミングを学びはじめると、こうした集計の課題にすぐ出会いますよね。
最初のうちは、forループを回して、今までの最大値より大きければ変数を書き換える、という書き方をしがちです。それでも正しく動きますが、Pythonには同じことを1行でこなせる道具が最初から用意されています。
それが、組み込み関数のmax()・min()・sum()です。どれもimportなしで使えて、最大値・最小値・合計をすぐに求められます。
今回は、この3つの関数の基本から、比べる基準を変えるkey引数、空のリストを渡したときのエラー、小数の合計で起きる誤差まで紹介します。サンプルの実行結果は、手元のPython 3.11で確かめたものです。
max()・min()・sum()は何をする関数か¶
まずは、3つの関数の役割をひとことで整理しておきましょう。名前は英語のmaximum、minimum、sumから来ていて、意味そのままの動きをします。
3つの関数の違いを、表にまとめてみました。
| 関数 | 返すもの | 空のリストを渡すと | よく使う追加の引数 |
|---|---|---|---|
| max() | いちばん大きい要素 | ValueErrorになる | key、default |
| min() | いちばん小さい要素 | ValueErrorになる | key、default |
| sum() | 要素をすべて足した値 | 0を返す | start |
表の真ん中の列は、あとで詳しく説明する大事なポイントです。同じ集計の仲間なのに、空のときの動きがmax()・min()とsum()で分かれています。
どの関数も、リストだけでなくタプルや集合、ジェネレータ式など、順番に要素を取り出せるものなら何でも受け取れます。こうした値のことを、Pythonではイテラブルと呼びます。
基本の使い方をコードで確かめよう¶
まずは、いちばんシンプルな使い方から見ていきましょう。ループで書いた場合と比べると、短くなるのがよくわかります。
scores = [72, 85, 64, 91, 58]
# ループで最大値を探す書き方
highest = scores[0]
for score in scores:
if score > highest:
highest = score
print(highest) # 91
# 組み込み関数を使う書き方
print(max(scores)) # 91
print(min(scores)) # 58
print(sum(scores)) # 370
# 平均点も1行で出せる
print(sum(scores) / len(scores)) # 74.0
ループ版では、最初の値を仮の最大値として置き、ひとつずつ比べて更新していました。max()を使えば、その手順をまるごと任せられます。
平均値は、sum()で出した合計をlen()で求めた個数で割るだけです。集計の基本は、この組み合わせでほとんどまかなえます。
引数を直接並べる書き方¶
max()とmin()は、リストを渡すだけでなく、値をカンマで直接並べることもできます。max(3, 8, 5)と書けば、8が返ってきます。
2つの値のうち大きいほうを選びたいときに便利です。たとえば、計算結果がマイナスにならないよう、max(0, balance)のように下限をつける書き方は実務でもよく使います。
ただし、sum()にはこの書き方がありません。sum(1, 2, 3)と書くとTypeErrorになるので、足したい値は必ずリストなどにまとめて渡しましょう。
文字列を比べるとどうなるか¶
max()とmin()は、数値だけでなく文字列にも使えます。文字列の場合は、辞書の並びのように先頭の文字から順に比べられます。
ここで少し注意したいのが、大文字と小文字の扱いです。Pythonでは文字に割り当てられた番号で比べるため、大文字のほうが小文字より小さいものとして扱われます。
そのため、min(['apple', 'Banana', 'cherry'])はBananaを返します。人間の感覚とはずれることがあるので、次に紹介するkey引数で比べ方をそろえるのがおすすめです。
key引数で比べる基準を変える¶
max()とmin()のいちばんの便利ポイントが、key引数です。keyに関数を渡すと、要素そのものではなく、その関数を通した結果で大小を比べてくれます。
文字列の長さで比べたり、辞書の特定の値で比べたりと、使える場面はたくさんあります。具体的なコードで見てみましょう。
words = ['apple', 'Banana', 'kiwi']
# 文字数がいちばん多い単語
print(max(words, key=len)) # Banana
# 大文字小文字を区別せずに比べる
print(min(words, key=str.lower)) # apple
students = [
{'name': '佐藤', 'score': 78},
{'name': '鈴木', 'score': 92},
{'name': '高橋', 'score': 85},
]
# 点数がいちばん高い生徒の辞書をまるごと取り出す
top = max(students, key=lambda s: s['score'])
print(top['name']) # 鈴木
ポイントは、keyで比べたあとに返ってくるのが、関数を通した値ではなく元の要素そのものだということです。最後の例では、点数の92ではなく、鈴木さんの辞書がまるごと返ってきます。
この性質のおかげで、いちばん点数の高い生徒の名前を知りたい、といった要望に1行で応えられます。lambdaの書き方に自信がない方は、次の記事もあわせて読んでみてください。
【関連記事】Pythonのlambda(ラムダ式)とは?
key引数の考え方は、並べ替えのsorted()とまったく同じです。sorted()の使い方を押さえておくと、max()とmin()のkeyも自然に読めるようになります。
【関連記事】Pythonのsorted()の使い方
辞書でいちばん大きい値のキーを探す¶
辞書をそのままmax()に渡すと、比べられるのは値ではなくキーです。{'a': 3, 'b': 5}を渡すと、キーの中で大きい'b'が返ってきます。
値がいちばん大きいキーを知りたいときは、keyに辞書のgetメソッドを渡すのが定番です。max(sales, key=sales.get)と書けば、売上がいちばん多い商品名を取り出せます。
辞書の扱いに慣れていない方は、こちらの記事で基本をおさらいしておくと安心です。
【関連記事】Pythonの辞書(dict)の使い方
同じ最大値が複数あるとき¶
最大値の要素が2つ以上あるとき、max()はどれを返すのでしょうか。答えは、先に出てきたほうです。
たとえば、同じ点数の生徒が2人いたら、リストの前にいる生徒が返されます。min()も同じで、最小値が並んだときは最初に見つかった要素が選ばれます。
空のリストでValueErrorになる落とし穴¶
ここからが、今回いちばん伝えたいポイントです。max()とmin()に空のリストを渡すと、ValueErrorというエラーで処理が止まります。
最大値を決める候補がひとつもないので、答えようがないというわけです。手元のPython 3.11では、max() arg is an empty sequenceというメッセージが表示されました。
私は10年ほどエンジニアとして開発に関わってきましたが、このエラーで夜間のバッチ処理を止めてしまったことがあります。テスト中は毎回データが入っていたのに、本番でたまたま対象が0件の日があり、そこでmax()が落ちてしまいました。
厄介なのは、データが入っているあいだは何の問題もなく動いてしまう点です。データが空になりうる場面では、空のときにどうするかを先に決めておくのが大切です。
default引数で空のときの値を決める¶
この落とし穴への対策として用意されているのが、default引数です。空のイテラブルを渡したときは、エラーにせずにdefaultの値を返してくれます。
書き方は、max(scores, default=0)のようにキーワード引数で渡すだけです。データがあるときはふつうに最大値を返し、空のときだけ0を返します。
デフォルト値には、Noneを指定することもできます。max(scores, default=None)としておけば、あとからif文で値がないケースを分けて扱えます。
なお、default引数が使えるのは、リストなどをひとつだけ渡したときに限られます。max(5, 3, default=0)のように値を直接並べた書き方と組み合わせると、TypeErrorになるので注意しましょう。
sum()の使い方と注意点¶
続いて、合計を求めるsum()を見ていきます。こちらは空のリストを渡しても、エラーにならずに0を返してくれます。
足し算の世界では、何も足さない状態が0だと考えるとイメージしやすいでしょう。そのため、max()やmin()のようなdefault引数は用意されていません。
start引数で初期値を足す¶
sum()には、start引数という2つ目の引数があります。これは、合計の出発点になる値です。
sum([1, 2, 3], start=10)と書くと、10に1、2、3を足した16が返ってきます。前月までの累計に今月の値を足し込むような場面で役に立ちます。
また、TrueとFalseは計算のうえでは1と0として扱われます。そのため、sum([True, False, True])は2になり、条件に合う件数を数える書き方としてもよく使われます。
文字列はsum()で連結できない¶
足し算という意味では、文字列も+でつなげられますよね。ところが、sum()で文字列を連結しようとするとTypeErrorになります。
エラーメッセージには、代わりに''.join(seq)を使うよう案内が表示されます。文字列をつなげたいときは、sum()ではなくjoin()を使いましょう。
小数の合計で起きる誤差¶
sum()でもうひとつ知っておきたいのが、小数を足したときの誤差です。0.1を10回足したら1.0になりそうですが、手元のPython 3.11では0.9999999999999999と表示されました。
これはsum()の不具合ではなく、コンピュータが小数を2進数で扱うために起きる誤差です。0.1のような値は、2進数ではぴったり表せないのが原因です。
誤差の仕組みについては、こちらの記事で詳しく解説しています。
【関連記事】Pythonの小数の計算誤差はなぜ起きる?
誤差を小さくしたいときは、標準ライブラリのmathモジュールにあるmath.fsum()が使えます。同じ0.1を10個並べたリストでも、math.fsum()なら1.0が返ってきました。
ちなみに、Python 3.12ではsum()が小数を足すときの計算方法が改良され、多くの環境で精度が上がっています。そのため、同じコードでもPythonのバージョンによって結果がわずかに変わることがあります。
| 方法 | 0.1を10個足した結果(Python 3.11) | 向いている場面 |
|---|---|---|
| sum() | 0.9999999999999999 | ふだんの集計 |
| math.fsum() | 1.0 | 小数の誤差を抑えたいとき |
お金の計算のように、1円の誤差も許されない場面ではdecimalモジュールを使うのが安心です。用途に合わせて道具を選びましょう。
実務で使うときのコツ¶
最後に、実務でmax()・min()・sum()を使うときのコツを2つ紹介します。どちらも、私が現場で何度も見てきたポイントです。
1つ目は、ジェネレータ式と組み合わせることです。sum(item['price'] for item in cart)のように書けば、いったんリストを作らずに合計を出せます。
角かっこで囲んだリスト内包表記でも動きますが、データが大きいときはジェネレータ式のほうがメモリにやさしくなります。この書き方は、any()やall()でも同じように使えます。
【関連記事】Pythonのany()とall()とは?
2つ目は、比べる値の型をそろえることです。数値と文字列が混ざったリストをmax()に渡すと、比べられずにTypeErrorになります。
CSVや画面の入力から読み込んだ値は、数字に見えても文字列のことがよくあります。集計する前に、int()やfloat()で数値に変換しておくと安全です。
まとめ¶
max()・min()・sum()は、最大値・最小値・合計を1行で求められる組み込み関数です。ループで変数を更新するよりも短く、何をしたいコードなのかがひと目で伝わります。
key引数を使えば、文字数や辞書の値など、好きな基準で比べられます。返ってくるのは元の要素そのものなので、いちばん点数の高い生徒をまるごと取り出す、といった使い方も簡単です。
そして、空のリストを渡すとmax()とmin()はエラーになることだけは覚えておいてください。データが空になりうる場面では、default引数を付ける習慣が、私のような夜間の呼び出しを防いでくれます。
まずは、手元のコードにある最大値探しのループを、1か所だけmax()に書き換えてみてはいかがでしょうか。
ここまでお読みいただきありがとうございました。