Last Updated: 2/6/2024, 5:44:57 AM

# if 文ってなに?

有るか、無いか
を判定しています

if 文をはじめて習うとき、 おそらく次のように教わったと思います。

条件が正しければ、処理を実行する。

if 1 + 1 == 2:
    ...  # 処理を実行する




しかし、もう少し詳しく見ると if 文そのものは 次のような判定をしています。

存在すれば、処理を実行する。
if obj:
    ...  # 処理を実行する





この文章では、根本的には Python の if 文は「有るか、無いか」を判定しているということを見ていただきます。

# 1. よく見かけるもの

Python では if 文の中に直接 bool 型 ではないオブジェクトを書くことができます。 初見だと何を意図しているのかわからなくて、そして戸惑います。

i = 0
if i:
    print('表示されません')

lst = []
if lst:
    print('表示されません')

dct = {}
if dct:
    print('表示されません')

s = ''
if s:
    print('表示されません')

obj = None
if obj:
    print('表示されません')
>>> i = 0
>>> if i:
...     print('表示されません')
... 
>>> lst = []
>>> if lst:
...     print('表示されません')
... 
>>> dct = {}
>>> if dct:
...     print('表示されません')
... 
>>> s = ''
>>> if s:
...     print('表示されません')
... 
>>> obj = None
>>> if obj:
...     print('表示されません')
... 
>>> 

ちなみに PEP 8 が、このように書くように定めています。

シーケンス(str, list, tuple) に対しては、空のシーケンスが偽と判断されることを利用してください。
For sequences, (strings, lists, tuples), use the fact that empty sequences are false.

Yes: if not seq:
     if seq:

No:  if len(seq):
     if not len(seq):

if 文の中で False と評価されるものは、次の通りです。

False
None
0
0.0
0j
''
()
[]
{}
set()
frozenset()

bool 関数に代入して実行すると全て False を投げ返されます。

bool(False)
bool(None)
bool(0)
bool(0.0)
bool(0j)
bool('')
bool(())
bool([])
bool({})
bool(set())
bool(frozenset())
>>> bool(False)
False
>>> bool(None)
False
>>> bool(0)
False
>>> bool(0.0)
False
>>> bool(0j)
False
>>> bool('')
False
>>> bool(())
False
>>> bool([])
False
>>> bool({})
False
>>> bool(set())
False
>>> bool(frozenset())
False
>>> 

if 文の中で True と評価されるものは、上記以外のものです。

以下の値: False 、 None すべての型における数値の 0、 空の文字列、空のコンテナ (文字列、タプル、リスト、辞書、集合、凍結集合など) は 偽 (false) であると解釈されます。 それ以外の値は真 (true) であると解釈されます。 6.11. ブール演算 (boolean operation) (opens new window)

「すべての型における数値の 0」というのは、様々な 0 があるということです。 例えば float 型の 0.0, complex 型の 0j などです。 より具体的には 数値型プロトコル (number protocol) (opens new window) を実装した型で 0 を表現しているものを指していると 思われます

# 1.1. if 文はいろんなことやりすぎじゃない?

このようにして様々な役割を if 文に与えることは PEP 20 あるいは、 直接は関係ありませんが UNIX 哲学で述べられているようなことに反してはいないでしょうか?

シンプルなものは、複雑なものよりよい。
Simple is better than complex.
The zen of Python - PEP 20 (opens new window)

個々のプログラムには、1つのことをちゃんとこなすようにさせる。
Make each program do one thing well.
UNIX哲学 - Wikipedia (opens new window)

答え: 反していないと感じます(※ あくまで、個人の感想です)。Python の if 文は、あるか、ないかを判定するという1つの機能を有しています。

いやいや 0 っていうオブジェクト、 空のリスト [] っていうオブジェクトが存在してるじゃないか?

という反論が聞こえてきそうですが、あくまでもそういう考え方だと捉えておいてください。 Python は、そのように実装されているように感じます。

# 2. False と評価されるオブジェクト

bool(var) == False となるオブジェクトをクラスごとに、実際に覗いてみましょう。

オブジェクトがあるか、ないか int という世界、型の中では 0 は無を表現しています。 また list という世界、型の中では [ ] は無を表現しています。

ユーザが定義したクラス、例えば User, Dog といったクラスを作った時に、 そのクラスのオブジェクトが存在しないことを表現する時は全て None を使います。

# 2.1. list, set, dict

つまり Python は "オブジェクトの集まりを表すオブジェクト" が、 1つもオブジェクトを持っていないとき __len__ が 0 のとき、 "オブジェクトの集まりを表すオブジェクト" を False と評価します。

"オブジェクトの集まりを表すオブジェクト" のわかりやすい例としては list, tuple, set, dict があると思います。 反対にわかりにくい例としては str があると思います。 1 文字ずつの文字の集まりと考えてください。例えば 'Hello' という単語は 'H', 'e', 'l', 'l', 'o' という文字の集まりです。

[c for c in 'Hello']
# ['H', 'e', 'l', 'l', 'o']

if 文で __len__ が評価される組み込み型の一覧です。

builtin_types = [cls for cls in __builtins__.__dict__.values() if isinstance(cls, type)]
builtin_sized_types = [cls for cls in builtin_types if hasattr(cls, '__len__') and not hasattr(cls, '__bool__')]
print(*builtin_sized_types, sep='\n')
<class 'memoryview'>
<class 'bytearray'>
<class 'bytes'>
<class 'dict'>
<class 'frozenset'>
<class 'list'>
<class 'set'>
<class 'str'>
<class 'tuple'>

# 2.2. int

int は __bool__ メソッドを持っており int.__bool__ は 真偽判定は 0 であれば False, それ以外は True となります。

>>> bool(0)
False
>>> bool(1)
True
>>> bool(2)
True
>>> bool(3)
True
>>> 

# 2.3. object

なぜ、クラスが __len__() も __bool__() も定義していないければ、 そのクラスのインスタンスはすべて真とみなされるのでしょうか?

クラスが __len__() も __bool__() も定義していないければ、 そのクラスのインスタンスはすべて真とみなされます。
object.__bool__ (opens new window)

それは "オブジェクトが存在しているから" です。 では、その逆のオブジェクトが存在していないとは、なんでしょうか? それが None です。なので None は False として扱われます。

class Cls:
    pass

if obj:
    print('Hello, world!')

obj = None
if obj:  # C クラスの None と考えることもできる。
    print('Nihao, shijie!')

これはどういうことかと言えば None は、オブジェクトが存在しないことを表現するオブジェクトです。0 か 1 かの 0 を表すオブジェクトです。

class Dog:
    pass

# 犬がいるか
dog = Dog()
if dog:
    print('Hello, world!')

# 犬がいないか
dog = None
if not dog:
    print('Nihao, shijie')

公式ドキュメントの記述。

None は、関数にデフォルト引数が渡されなかったときなどに、値の非存在を表すのに頻繁に用いられます。
3. 組み込み定数 - Python 標準ライブラリ (opens new window)

以下は、海外ブログの記述。

None is as the name suggest nothing
and similar to 0 and {}.
What is 'None' data type in Python? (opens new window)

ここで大事なのは、様々なユーザ定義クラスのオブジェクトが "存在しないこと" を表現する時に None を使うということです。

そして None には、もうひとつ別の使い方があります。 それは "未定義であること" を表現するために使われたりもします。

それについては try 文ってなに? のなかでご説明させていただきたいと 考えております。

# 2.4. bool

なるほど、str, list, dict, object, int は、あり、なしを判定しているというのは、 わかったけど肝心の True と False はどうなの?という疑問が残ります。

実は bool は int を継承しています。

bool.__bases__ == (int, )

実際 False, True は、それぞれ 0, 1 と等値です。

(False, True) == (0, 1)

つまり bool にしても if 文では極論すれば あり、なし を判定しています。 bool が、なぜ int を継承したのかについては、こちらに書きました。

bool.__bool__ は、int から継承した int.__bool__ を参照するようになっています。

>>> int.__bool__ is bool.__bool__
True
>>>
>>> help(int.__bool__)
Help on wrapper_descriptor:

__bool__(self, /)
    self != 0
(END)
>>>

bool は int から継承して __bool__ を借用しているので bool.__bool__ の説明は、int.__bool__ と同じですね。

>>> help(bool.__bool__)
Help on wrapper_descriptor:

__bool__(self, /)
    self != 0
(END)
>>>

ちなみに __bool__(self, /) の '/' は、なんでしょうか?位置引数であることを明示するために書かれてる気配があります。ただ PEP 457 の status は draft で正式採用はされていないので実際に使おうとすると SyntaxError になります。

>>> def __bool__(sef, /):
  File "<stdin>", line 1
    def __bool__(sef, /):
                      ^
SyntaxError: invalid syntax
>>> 

# 3. if 文の仕組み

どうやって、あるかないかを判定しているの?
答え: bool クラスをインスタンス化しています。

# これが実行されると
if obj:
    ...  # 処理を実行する
# これが実行される
if bool(obj):
    ...  # 処理を実行する

# 3.1. __bool__ メソッド

また、適当な疑似コード見せて、本当にそうなの?って感じですが 以下のコードを実行してみてください。 if 文などで条件分岐をする際、論理演算を実行しています。内部で __bool__ メソッドが呼ばれています。

class Cls:
    def __bool__(self):
        print('Hello, world!')
        return True

obj = Cls()
if obj:
    pass
>>> # if 文が実行された際に
>>> # Hello, world! が出力されています。
>>> obj = Cls()
>>> if obj:
...     pass
... 
Hello, world!
>>> 

# 3.2. メソッド探索の順番

__bool__ メソッドがない時は、__len__ メソッドが呼ばれます。 __len__ メソッドもない時は True が返されます。 assert 文は True なら何もしませんが False だと AssertionError を返すテストのための文です。

# assert 文は False の場合 AssertionError を投げます。

# 1. __bool__ メソッドが評価される。
class A:   
    def __bool__(self):
        return False
    
    def __len__(self):
        return 0

assert not A()



# 2. __bool__ メソッドがない時は、
#   __len__ メソッドが呼ばれる。
class B:
    def __len__(self)
        return 0

assert not B()


# 3. __len__ メソッドもない時は True が返されます。
class C:
    pass

assert C()

真理値テストや組み込み演算 bool() を実装するために呼び出されます; False または True を返さなければなりません。 このメソッドが定義されていないとき、 __len__() が定義されていれば呼び出され、 その結果が非 0 であれば真とみなされます。 クラスが __len__() も __bool__() も定義していないければ、 そのクラスのインスタンスはすべて真とみなされます。
object.__bool__ (opens new window)

こうして __len__ メソッドを呼び出しているのを見ると、 if 文というのは極論すればオブジェクトの有無を評価しているようにも見えます。

また __len__ が無ければ True を返すのも、 明示的に False を返さなければ、 オブジェクトが存在する、ということで True を返しているように見えます。

# 4. JavaScript

# 4.1. if 文の評価

それどころか空のリスト []true と評価されたりして、一体なにを基準にして turefalse だか判別が付きにくくなっています。 Guido やその他の Python の開発者たちの配慮が伝わればと思います。

JavaScript - null とか undefined とか ...
の判定について - Qiita
type value result
真偽値 false false
真偽値 true true
数値 0 false
数値 1 true
文字列 "" false
文字列 "a" true
配列 [] true
配列 [0] true
null null false
オブジェクト{} true
undefined undefined false

ちなみに {} は、JavaScript では辞書ではなくオブジェクトです。 Python で言えば、インスタンス変数を持たない以下のコードの obj に近いものです。

class Cls:
    pass

obj = Cls()
obj == True
>>> obj == True
True
>>>

# 4.2. null と undefined

上の表で null とあります。 これは Python の None と同じです。 JavaScript の null, Python の None は、 それぞれオブジェクトが「無い」ことを表します。

では undefined とはなんでしょうか? undefined はオブジェクトが「未定義」であることを表します。 Python は None を使って「未定義」を表します。

実は Python の None には2つの使われ方があります。 1つ目は、オブジェクトが「無い」こと。 2つ目は、オブジェクトが「未定義である」こと。

Python を使っている方は無意識のうちに、 この2つのことするために None を使っています。

これは実は結構重たいテーマだと思っています。 この2つの None については、この先にある記事 try 文ってなに? の中で、 ご紹介させていただきたいと思います。

# 5. まとめ

if 文は

有るか、無いか
を判定しています