CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: 条件分岐と真偽値周りの話

今回は Python の条件分岐と真偽値周りの話について。 ざっくりと内容をまとめると次の通り。

  • Python の条件分岐には真偽値以外のオブジェクトを渡せる
    • 意味的には組み込み関数 bool() にオブジェクトを渡すのと等価になる
  • ただし条件分岐に真偽値以外のオブジェクトを渡すと不具合を生みやすい
    • そのため、条件分岐には真偽値だけを渡すようにした方が良い
  • なお、オブジェクトを bool() に渡したときの振る舞いはオーバーライドできる
    • 特殊メソッド __bool__() を実装すれば良い

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.4
BuildVersion:   18E226
$ python -V
Python 3.7.3

下準備

今回の説明は Python の REPL を使って進めていくので、あらかじめ起動しておく。

$ python

Python の条件分岐について

Python の条件分岐には真偽値 (bool ) 型以外のオブジェクトも渡せる。 例えば、次のコードは Python においてちゃんと動作する。 関数 f() における引数 x は bool 型である必要もない。

>>> def f(x):
...     # 引数が有効か無効かを判断するつもりの条件分岐
...     if x:
...         print('Valid')
...     else:
...         print('Invalid')
... 

では、上記の引数 x に色々なオブジェクトを渡すと、どのように振る舞うだろうか。 ちょっと見てみよう。

例えば真偽値型の True を渡してみる。 これは、当然ながら上のコードブロックに遷移する。

>>> f(True)
Valid

では、長さのある文字列だったら? これも、上のコードブロックに遷移する。

>>> f('Hello, World!')
Valid

非 0 の整数なら? これまた同様。

>>> f(1)
Valid

では、続いて None を渡してみよう。

>>> f(None)
Invalid

この場合は、下のコードブロックに遷移した。 なんとなく、ここまでは直感どおりに思える。

じゃあ長さのない文字列 (空文字) を渡したらどうなるだろう。

>>> f('')
Invalid

なんと、この場合は下のコードブロックに遷移してしまった。

整数としてゼロを渡した場合も同様。

>>> f(0)
Invalid

では、上記の不思議な振る舞いは一体何に由来するものだろうか。 実はオブジェクトを条件分岐に渡すとき、意味的には組み込み関数 bool() に渡すのと等価になる。

つまり、最初に示した関数 f() は、次のコードと等価ということになる。

>>> def f(x):
...     # オブジェクトの真偽値表現を組み込み関数 bool() で取得する
...     if bool(x):
...         print('Valid')
...     else:
...         print('Invalid')
... 

組み込み関数 bool() では、オブジェクトを真偽値として評価した場合の結果が得られる。 先ほど試したいくつかのオブジェクトを実際に渡してみよう。

>>> bool('')
False
>>> bool(' ')
True
>>> bool(0)
False
>>> bool(1)
True

上記で得られる返り値の内容は、先ほどの検証で得られた振る舞いと一致する。

このように、真偽値以外のオブジェクトを条件分岐に渡すと直感的でない振る舞いをすることがある。 コードの直感的でない振る舞いは不具合につながる。 また、コメントでもない限り、意図してそのコードにしているのかも分かりにくい。

PEP20: Zen of Python にある Explicit is better than implicit. を実践するのであれば、真偽値を渡すほうが良いと思う。 例えば、最初のコードで仮に「None か否か」を判定したいのであれば、次のようにした方が良いと考えられる。

>>> def f(x):
...     # オブジェクトが None か判定結果を真偽値として得る
...     if x is not None:
...         print('Valid')
...     else:
...         print('Invalid')
... 

... is not None は対象が None かそうでないかを真偽値で返すことになる。 解釈にブレが生じることはない。

>>> 'Hello, World!' is not None
True
>>> '' is not None
True
>>> 1 is not None
True
>>> 0 is not None
True
>>> None is not None
False

ちなみに、自分で定義したクラスのインスタンスが組み込み関数 bool() に渡されたときの振る舞いはオーバーライドできる。 具体的には特殊メソッド __bool__() を実装すれば良い。

以下のサンプルコードでは、クラス FizzBuzz に特殊メソッドを定義して振る舞いをオーバーライドしている。 このクラスのインスタンスは渡された整数の値によって組み込み関数 bool() から得られる結果を切り替える。

>>> class FizzBuzz(object):
...     """整数が 3 か 5 で割り切れる値か真偽値で確認できるクラス"""
...     def __init__(self, n):
...         self.n = n
...     def __bool__(self):
...         # Fizz
...         if self.n % 3 == 0:
...             return True
...         # Buzz
...         if self.n % 5 == 0:
...             return True
...         # Others
...         return False
... 

引数が 35 で割り切れるときに True を返し、それ以外は False になる。

>>> o = FizzBuzz(3)
>>> bool(o)
True
>>> o = FizzBuzz(5)
>>> bool(o)
True
>>> o = FizzBuzz(4)
>>> bool(o)
False

いじょう。