背景
Pythonでちょっとしたツールを作ってて、テストを書いてたら Github Actionsで設定していたRuff
(Pythonの静的解析ツール)にこう怒られました。
E712: Comparison to True should be 'if cond:' or 'if not cond:'
(この E712
は flake8
(のベースになっている pycodestyle
)に由来するルール)
これが、Pythonにおける、x, x == True, x is True の違いについて考えるきっかけになったので、そのメモを残します。
なにがダメなの?
Ruffが怒ってるのは、比較演算子 == True
が冗長で非推奨っていう理由です。
具体的には、
if hoge == True
は嫌で
if hoge
がいいっていう話。
うーん でも
if hoge == True
も 「明示的で良いのでは?」 そこまで怒る必要あるんかな
(Javaとか、SQLの==TRUEとか, Cの==1イメージ)
なぜ == True
がまずいのか?
1. 冗長でPythonicじゃない
PEP8(Pythonのスタイルガイド)https://peps.python.org/pep-0008/ でも、
Don't compare boolean values to True or False using
==
.
つまり
Python では「真偽値の判定」はそのまま書くのが自然。
# Correct:
if is_active:
# Wrong:
if is_active == True:
# Worse:
if is_active is True:
Pythonらしくないってだけなら「それはそうか」な話...
どう書くのが正解か?
改めてPEP8
Don't compare boolean values to True or False using
==
.Yes:
if greeting:
No:if greeting == True:
Worse:if greeting is True:
ほんまに?
PEP 8ではis True
はさらに悪い書き方だとされてるけど
実際にはTrue
以外の「Truthy(真と評価される値)」な値(例: 1
や空でない文字列)と、純粋なTrue
を区別したい場面も存在するので
その辺も踏まえてまとめると
- Truthy:1 や "abc" のように True として扱われる値
- Falsy:0 や None のように False として扱われる値)
判定の種類 | 書き方 | 推奨度 |
---|---|---|
Truthyか? |
if x: / assert x
|
⭐️⭐️⭐️⭐️⭐️ |
Falsyか? |
if not x: / assert not x
|
⭐️⭐️⭐️⭐️⭐️ |
厳密にTrueか? |
if x is True: / assert x is True
|
⭐️⭐️⭐️(必要なら) |
Trueと等しいか? |
if x == True: / assert x == True
|
⭐️(避けるべき) |
うーん PEP8とは少し評価が違うが、観点が違うということで。
(⭐️は筆者の主観(可読性・バグリスク基準))
補足①
is True
は、「True
かもしれないし、None
や他の型の値も返ってくる可能性がある」 といった、関数の戻り値の型が複数ありえるケースで、厳密な型チェックをしたい場合に有効な手段だと考えて⭐️3つ
補足② 真偽値表
3つとも振る舞いが違う
== Trueの振る舞いは boolがintのサブクラスであること(PEP285)を押さえておけばよさそう
値 |
== True の結果 |
is True の結果 |
Truthy? (if x ) |
理由 |
---|---|---|---|---|
1 |
✅ True | ❌ False | ✅ True |
bool は int のサブクラス。1 == True は True だが同一オブジェクトではない。 |
"yes" |
❌ False | ❌ False | ✅ True | 空でない文字列は Truthy。等価比較は型が違うので False。 |
[1] |
❌ False | ❌ False | ✅ True | 空でないリストは Truthy。list == True は False。 |
True |
✅ True | ✅ True | ✅ True | 真偽値そのもの。 |
1.0 |
✅ True | ❌ False | ✅ True |
1.0 == 1 == True が成り立つ(数値比較連鎖)。同一性は成立しない。 |
0 |
❌ False | ❌ False | ❌ False |
0 は Falsy。0 == True は False。0 == False は True。 |
None |
❌ False | ❌ False | ❌ False |
None は Falsy。 |
False |
❌ False | ❌ False | ❌ False |
False == True は False。False is True も False。 |
(1) `"yes" == True` が False なのは、より厳密には、左辺の `str.__eq__` も右辺(`bool` は `int` のサブクラス)側の `int.__eq__` も互いを等価と判定しないため。
結局、JAVAやCと違って任意のオブジェクトに真偽値を適用できるので、こういう感じになってますね、おそらく
おまけ
注意として、__eq__
や __bool__
がオーバーロードされているユーザー定義クラスのインスタンスは、振る舞いが変わってきます。(例えばnumpyのndarray, PandasのSeries)
import numpy as np
a = np.array([True, False, True])
result_True = a == True
result_False = a == False
print(result_True) # → [ True False True ]
print(result_False) # → [ False True False ]
print(type(result_True)) # → <class 'numpy.ndarray'>
print(type(result_False)) # → <class 'numpy.ndarray'>
つまり a == True
は bool
じゃなくて ndarray(bool)
が返ってきます。
numpyにおいては意図に応じて明示的に書く必要があります:
意図 | 書き方 |
---|---|
すべての要素が True か? | assert a.all() |
いずれか1つでも True か? | assert a.any() |
assert a
や if a:
のように書いても、同じValueError
が発生します。
(この挙動は、pandas
のSeries
など、他のライブラリでも共通しています。「暗黙の真偽値評価」ができないのです。)
このへんが Python のリストや普通の変数と違ってややこしいところ。
まとめ
-
assert x == True
は Python では非推奨(E712) - 真偽値の比較は
assert x
/assert not x
が基本 -
is True
を使いたいときは「型まで厳密に見る」場面だけ - ユーザー定義クラスのインスタンスは、boolの振る舞いの仕様が違うことがあるよ
参考リンク
PEP 285 を読むと「なぜ bool
を int
のサブクラスとして導入したか」「if x == True
を仕様側で特別扱いしなかったか」という経緯が書かれていて結構面白いです。