[Pythonクイズ]Python特有? こんな書き方できるって知ってましたか?:Pythonステップアップクイズ
ある値が特定の範囲に含まれるかどうかを調べるときってありますよね。そんなときにはどう書いていますか? もしかしたら、いつものやり方以外にもいい方法があるかもしれませんよ?
【問題】
以下はユーザーに整数を入力してもらい、その値が0以上10未満であれば「OK」と、そうでなければ「NG」と表示するPythonのコードである。このコードは問題なく動作する。しかし、このコードにはほんの少しシンプルに書ける部分がある。それはどこだろう。なお、ユーザーが整数以外の値を入力した場合の例外処理については考慮しなくてもよいものとする。
num = input('input 0-9: ')
num = int(num) # 例外は考慮しなくてもよい
if 0 <= num and num < 10:
print('OK')
else:
print('NG')
どうもHPかわさきです。
編集会議では「今回の問題、解説パートにはあんまり書くことないかもねぇ」などといっていたんですが、実は思ったよりも書くことがありました。それでも、ここ最近の中ではちょっと短めです。短い方がいいですね。太く短く生きていきたいです(ダイエットを始めてから、体重的にはもうそれほど太くなくなっちゃったんですが)。
【答え】
正解のコード例を以下に示します。
num = input('input 0-9: ')
num = int(num) # 例外は考慮しなくてもよい
if 0 <= num < 10:
print('OK')
else:
print('NG')
単純に数値が特定の範囲にあるかどうかを調べたいというとき、Pythonでは「0 <= num and num < 10」のように比較演算子をand演算子でつなぐような書き方をせずに、「0 <= num < 10」のように記述できます。
なお、以下のようなコードを考えた方もいらっしゃるでしょう。
num = input('input 0-9: ')
num = int(num) # 例外は考慮しなくてもよい
if num in range(0, 10):
print('OK')
else:
print('NG')
もちろん、これも正解です。後で少し取り上げましょう。他にも正解となるコードはあるかもしれません。あったら、教えてください。
【解説】
Pythonのドキュメントに目を通すと、「比較演算子」の説明に「比較はいくらでも連鎖することができます。例えば x < y <= z は x < y and y <= z と等価になります」とあります。これが全てであり、これ以上は特に説明の必要はないかもしれません。ただし、ここでいう「等価」とは「True/Falseのどちらを返すのか」という振る舞いに関しての話であることには注意してください。詳しくは後述します。
このような書き方ができる言語はあまりないようです。そういうわけで、記事のタイトルには「Python特有?」と入れてみました。調べてみたところ、Juliaでも比較の連鎖は可能です。
問題文のコードは以下の通りでした。
num = input('input 0-9: ')
num = int(num) # 例外は考慮しなくてもよい
if 0 <= num and num < 10:
print('OK')
else:
print('NG')
この中のif文の条件は「0 <= num and num < 10」であり、引用した文から考えると、この式は「0 <= num < 10」と等価です。このことから正解例のコードが出てくるわけです。
num = input('input 0-9: ')
num = int(num) # 例外は考慮しなくてもよい
if 0 <= num < 10:
print('OK')
else:
print('NG')
ただし、「True/Falseのどちらを返すか」という振る舞いについては等価なのですが、違うところもあります。ドキュメントでは先の1文に続けて「ただしこの場合、前者ではyはただ一度だけ評価される点が異なります」とあるのです。どういうことか試してみましょう。以下のコードを見てください。
# 比較演算子を連鎖させた書き方
num = 8
if 0 <= (num := num + 1) < 10:
print('OK')
else:
print('NG')
print(num)
# and演算子を使う一般的な書き方
num = 8
if 0 <= (num := num + 1) and (num := num + 1) < 10:
print('OK')
else:
print('NG')
print(num)
この2つのif文では条件が「0 <= (num := num + 1) < 10」と「0 <= (num := num + 1) and (num := num + 1) < 10」となっています。「(num := num + 1)」という代入式をXと置き換えると、これは「0 <= X < 10」「0 <= X and X < 10」なので(真偽値を返すという意味での)等価な関係は成り立っています。しかし、前者のコードでは式が一度だけ評価される点が異なります。逆にいえば、後者のコードでは代入式は二度評価されるということです。
実際に両者のコードを実行した結果を以下に示します。
実行結果が違っている点に注目してください。上のコードでは変数numの初期値を8としています。そのため、1つ目のif文では代入式は一度だけ評価され、変数numの値は9になります。よって、「OK」と表示されました。一方、2つ目のif文では代入式が二度評価されるので、変数numの値は10になって「NG」と表示されたというわけです。
代入式をこんなふうに使うことがあるかどうかはともかくとして、比較演算子を連鎖させる書き方をするときには、and演算子を使う場合とは式が評価される回数が異なる点には注意してください。
なお、条件を構成する式を「exp0、exp1、exp2」と、演算子を「op0、op1」と書いたとき、「exp0 op0 exp1 op1 exp2」が「exp0 op0 exp1 and exp1 op1 exp2」と等価ということですから、実は次のような書き方もできちゃいます。
num = 5
if 0 < num != 2:
print('larger than 0 and not equal to 2')
else:
print('smaller than or equal to 0 or equal to 2')
この条件は「0より大きくて、2ではない」のであればTrueに、そうでなければ(0以下か、2なら)Falseになります。しかし、この条件であれば、次のように書いた方が意味が伝わりやすいでしょう。
num = 5
if 0 < num and num != 2:
print('larger than 0 and not equal to 2')
else:
print('smaller than or equal to 0 or equal to 2')
「Pythonではこういう書き方ができる」からといって、必ずしもそういう書き方をしなければならないわけではありません。コードの読みやすさを重視して、演算子の連鎖の使い過ぎにはご注意ください。
最後にもう一つの正解についても簡単に触れておきましょう。
num = input('input 0-9: ')
num = int(num) # 例外は考慮しなくてもよい
if num in range(0, 10):
print('OK')
else:
print('NG')
このコードのif文では変数numの値がrange(0, 10)オブジェクトの要素(0から9の整数値)に含まれるかどうかをin演算子で判定しています。in演算子とrangeオブジェクトの組み合わせはPython的ともいえますよね。
でも、「rangeオブジェクトの要素の一つ一つと比較していたら、時間がかかるんじゃない?」と思った方がいるんじゃないでしょうか。実際のところはそうはなっていないようです。簡単にいってしまうと、「x in range(……)」はrangeオブジェクトの全要素とxを一つ一つ比べるのではなく(リニア探索)、数値計算によって結果を得るようになっています。そのため、比較演算子を連鎖させる方法に比べると若干遅くなりますが、それでも高速に結果を得られます。
あと、浮動小数点数値が特定の範囲にあるかどうかを調べるのに、この方法を使うとどうなるか。筆者はそこまでは考えていませんでした。興味のある方はご自分で試してみてください。
他にも「これだ!」という方法が思い浮かんだ方はHPかわさきまで教えてくださいね。
Pythonの演算子については「Python入門」の「Pythonの演算子まとめ」をご覧いただければと思います。
Copyright© Digital Advantage Corp. All Rights Reserved.