リストの中に、ひとつでも60点未満の点数が混ざっていないか確かめたい。そんな場面に出会ったことはありませんか。
Pythonを学びはじめたころは、forループを回して、見つかったらフラグ変数をTrueにする書き方をしがちです。もちろんそれでも動きますが、行数が増えて、何を確かめたいコードなのかが少し見えにくくなります。
そこで役に立つのが、組み込み関数のany()とall()です。どちらもimportなしで使えて、ひとつでも当てはまるか、全部が当てはまるかを1行で判定できます。
今回は、any()とall()の基本から、ジェネレータ式と組み合わせる書き方、空リストのときの意外な結果まで、手元のPython 3.11で動かした結果とともに紹介します。ループで書いていたコードがどう変わるのか、一緒に見ていきましょう。
any()とall()は何をする関数か¶
まずは、それぞれの役割をひとことで押さえておきましょう。any()は、渡したものの中にひとつでも真の値があればTrueを返す関数です。
all()はその逆の発想で、渡したものがすべて真のときだけTrueを返します。英語のanyとallの意味そのままなので、名前から動きを想像しやすいですね。
2つの関数の違いを、表で並べてみました。
| 関数 | Trueを返す条件 | Falseを返す条件 | 空のときの結果 |
|---|---|---|---|
| any() | 真の要素がひとつでもある | すべての要素が偽 | False |
| all() | すべての要素が真 | 偽の要素がひとつでもある | True |
表のいちばん右の列は、あとで詳しく説明する大事なポイントです。ここでは、空のときに結果が分かれることだけ覚えておいてください。
どちらの関数も、Python 2.5で追加された古くからある組み込み関数です。長く使われてきたぶん、実務のコードでもよく見かけます。
真の値と偽の値をおさらいしよう¶
any()とall()を正しく使うには、Pythonが何を真とみなし、何を偽とみなすかを知っておく必要があります。ここで一度おさらいしておきましょう。
Pythonでは、False、None、0、空の文字列、空のリストや辞書などが偽として扱われます。それ以外の値は、基本的にすべて真です。
実際に動かして確かめてみます。
print(any([0, "", None])) # False(全部が偽)
print(all([1, 2, 3])) # True(全部が真)
print(all(["0", "False"])) # True(中身があるので文字列は真)
3行目に注目してください。文字列の0やFalseは、見た目こそ偽っぽいですが、空ではないので真として扱われます。
公式ドキュメントのヘルプにも、all()はすべての値xについてbool(x)がTrueのときにTrueを返す、と書かれています。つまり中身はbool()で判定されているわけです。
真偽値の扱いは、if文でもつまずきやすいところです。詳しく知りたい方は、こちらの記事も参考にしてみてください。【関連記事】Pythonの真偽値を深く知る!if文で意外とハマるNoneや空リストの判定
フラグ変数のループをany()とall()で書き換える¶
ここからは、実際によくあるコードを書き換えてみます。どれくらいすっきりするのか、ビフォーアフターで比べてみましょう。
ひとつでも条件に合うかをany()で確かめる¶
テストの点数の中に、90点以上がひとつでもあるかを調べたいとします。ループとフラグ変数で書くと、次のようになりますね。
scores = [72, 85, 90, 64]
has_high_score = False
for s in scores:
if s >= 90:
has_high_score = True
break
print(has_high_score) # True
同じ判定をany()で書くと、たった1行になります。
scores = [72, 85, 90, 64]
print(any(s >= 90 for s in scores)) # True
かっこの中の s >= 90 for s in scores は、ジェネレータ式と呼ばれる書き方です。リスト内包表記の角かっこを外した形だと考えると、イメージしやすいと思います。
この1行は、scoresの中に90以上のsがあるか、と英文のように読めます。フラグ変数の初期値やbreakの位置を気にしなくてよいのも、うれしいところです。
全部が条件を満たすかをall()で確かめる¶
今度は、全員が60点以上で合格しているかを確かめてみましょう。all()を使うと、次のように書けます。
scores = [72, 85, 90, 64]
print(all(s >= 60 for s in scores)) # True
ループで書く場合は、最初にTrueを入れておき、条件に合わない要素が見つかったらFalseにする流れになります。any()のときと初期値が逆になるので、書き間違えやすいポイントでした。
all()を使えば、その初期値の心配がなくなります。読む人にとっても、全員が合格しているか、という意図がひと目で伝わるはずです。
リスト内包表記にまだ慣れていない方は、先にこちらを読んでおくと理解がぐっと楽になります。【関連記事】Pythonのリスト内包表記を使いこなせ!3行のループを1行に
結果が決まった時点で判定を打ち切る仕組み¶
any()とall()には、もうひとつ大きな長所があります。答えが決まった時点で、残りの要素を見ずに判定を終えてくれるのです。
これを短絡評価と呼びます。any()なら最初の真の値、all()なら最初の偽の値が見つかった瞬間に止まります。
本当に途中で止まっているのか、確かめるコードを用意しました。判定のたびにメッセージを表示する関数を作って、all()に渡してみます。
def check(x):
print("check", x)
return x > 0
print(all(check(x) for x in [3, -1, 5]))
実行すると、次のように表示されました。
check 3
check -1
False
-1でFalseが確定したので、5はチェックされていませんね。データが数十万件あっても、早い段階で答えが出れば、そのぶん処理は短く済みます。
角かっこを付けると打ち切りが効かなくなる¶
ここで、初心者がよくやってしまう書き方を紹介します。ジェネレータ式ではなく、角かっこを付けてリスト内包表記にしてしまうパターンです。
print(all([check(x) for x in [3, -1, 5]]))
結果はどちらもFalseですが、こちらはcheck 5まで表示されます。リストを作る段階で、全要素の計算が先に終わってしまうからです。
結果が同じなので、間違いに気づきにくいのが厄介なところです。基本は角かっこを付けずに渡す、と覚えておくと安心ですね。
ジェネレータが1件ずつ値を作る仕組みは、こちらの記事で詳しく解説しています。【関連記事】ジェネレータ(yield)っていつ使うの?巨大なデータをメモリに優しく扱う方法
空リストのall()がTrueになる落とし穴¶
ここからが、今回いちばん伝えたい内容です。空のリストをall()に渡すと、結果はTrueになります。
items = []
print(all(x > 0 for x in items)) # True
print(any(x > 0 for x in items)) # False
え、何も入っていないのに全部OKなの?と驚いた方もいるかもしれません。これは、条件に合わない要素がひとつも見つからなかった、という意味でTrueになっていると考えると納得しやすいです。
数学や論理学では、こうした真を空虚な真と呼ぶことがあります。ルールとしては正しいのですが、実務では思わぬバグにつながることがあるのです。
実務でハマったデータが空のケース¶
私は10年ほどエンジニアとして開発に関わってきましたが、この落とし穴で痛い目を見たことがあります。全件のチェックが通ったら次の処理へ進む、というバッチ処理で、取り込み元のデータが空なのに成功扱いで流れてしまいました。
原因は、ファイルの読み込みに失敗して空のリストになっていたことでした。all()は何も言わずにTrueを返したので、エラーに気づくまで半日ほどかかったのを覚えています。
それ以来、空になる可能性があるデータには、空でないことの確認を先に入れるようにしています。書き方は次のとおりです。
items = []
ok = bool(items) and all(x > 0 for x in items)
print(ok) # False
bool(items)がFalseなら、andの右側は実行されずにFalseが返ります。空のときにどう扱いたいかを、コードの上で明示しておくのがポイントです。
空なら何もしないで終わる処理なら、関数の冒頭で早めにreturnしてしまうのもよい方法です。【関連記事】早期リターンでネストを浅く! 読みづらいif文をスッキリさせる書き方
実務でよく使うany()とall()のパターン¶
基本がわかったところで、実際の開発でよく出てくる使い方を見ていきましょう。どれも、知っておくとすぐに使えるものばかりです。
まずは、ログの中にエラーの行がひとつでもあるかを調べる例です。in演算子と組み合わせると、自然な文章のように書けます。
lines = ["start", "ok", "an error occurred", "end"]
if any("error" in line for line in lines):
print("エラーを含む行があります")
次は、入力フォームの必須項目がすべて埋まっているかを確かめる例です。辞書のget()と組み合わせると、キーがない場合にも対応できます。
user = {"name": "Sato", "email": "", "age": 30}
required = ["name", "email", "age"]
if not all(user.get(k) for k in required):
missing = [k for k in required if not user.get(k)]
print("未入力の項目:", missing) # 未入力の項目: ['email']
all()で全体を判定し、足りないときだけ内包表記で原因を探す流れです。判定と原因探しを分けておくと、それぞれのコードが短く保てます。
このほかにも、よく使う組み合わせを表にまとめてみました。
| やりたいこと | 書き方の例 |
|---|---|
| 特定の拡張子のファイルがあるか | any(name.endswith(".csv") for name in files) |
| すべて数字だけの文字列か | all(s.isdigit() for s in values) |
| 禁止ワードが含まれていないか | not any(w in text for w in ng_words) |
| リストがすべて同じ型か | all(isinstance(x, int) for x in data) |
3行目のように、not any()と書くと、ひとつも含まれていない、という判定になります。not all()なら、少なくともひとつは条件に合わない、という意味です。
何が見つかったかを知りたいときは別の方法を¶
any()は便利ですが、返ってくるのはTrueかFalseだけです。どの要素が条件に合ったのかまでは教えてくれません。
最初に見つかった要素そのものがほしいときは、next()とジェネレータ式を組み合わせるのが定番です。
data = [1, 3, 8, 2, 10]
first_big = next((x for x in data if x > 5), None)
print(first_big) # 8
next()の2つ目の引数は、見つからなかったときに返す値です。これを省略すると、見つからないときにStopIterationというエラーが出るので注意してください。
用途ごとの使い分けを、表で整理しておきます。
| 知りたいこと | 使うもの | 返ってくるもの |
|---|---|---|
| ひとつでもあるか | any() | TrueかFalse |
| 全部そうか | all() | TrueかFalse |
| 最初の1件はどれか | next()とジェネレータ式 | 見つかった要素か既定値 |
| 条件に合うものをすべて | リスト内包表記 | 新しいリスト |
ループの途中で見つかったら処理を変えたいときは、for-else文で書く方法もあります。any()で足りるかどうか迷ったら、何を結果として受け取りたいのかを考えてみてください。【関連記事】for-else 文とは?else は if だけじゃない、ニッチで便利な構文
any()とall()を使うときの注意点¶
最後に、使うときに気をつけたい点を2つ紹介します。どちらも、知っていれば避けられるものです。
1つ目は、条件を詰め込みすぎないことです。かっこの中に複雑な条件を何個も並べると、1行で書けても、かえって読みにくくなってしまいます。
条件が長くなったら、判定用の小さな関数に切り出しましょう。all(is_valid(row) for row in rows)のように書けば、意図がぐっと伝わりやすくなります。
2つ目は、ジェネレータを使い回さないことです。一度any()やall()に渡したジェネレータは、途中まで、あるいは最後まで消費されています。
同じジェネレータをもう一度渡しても、残りの要素しか判定されません。何度も判定したいデータは、リストとして持っておくか、そのつどジェネレータ式を書き直すのが安全です。
まとめ¶
any()とall()は、ひとつでもあるか、全部そうかという判定を1行で書ける組み込み関数です。フラグ変数を使ったループよりも短く、意図が伝わりやすいコードになります。
ジェネレータ式と組み合わせれば、答えが決まった時点で判定が止まります。角かっこを付けずに渡すことを、ぜひ習慣にしてみてください。
そして、空のリストではall()がTrueを返すことだけは忘れないでください。データが空になりうる場面では、空のときの扱いを先に決めておくと、私のように半日を溶かさずに済みます。
まずは、手元のコードにあるフラグ変数のループを、1か所だけany()かall()に書き換えてみてはいかがでしょうか。読み返したときのすっきり感を、きっと実感できるはずです。
ここまでお読みいただきありがとうございました。