Python の if 文ってなに?





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







f:id:domodomodomo:20161101214858p:plain





実は Python の if 文には2つの意味に分けられるかなと思います。






① True ならば
処理を実行する。


if True:
    ...  # 処理を実行する





② オブジェクトが存在すれば
処理を実行する。


if obj:
    ...  # 処理を実行する




この記事は2番目について説明します。実は bool は int を継承しているので、1番目も2番目も同じことを言っています。

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


よく見かけるもの

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

i = 0
if i:
    print('Hello, world!')

lst = []
if lst:
    print('Hello, world!')

dct = {}
if dct:
    print('Hello, world!')

s = ''
if s:
    print('Hello, world!')

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


ちなみに 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
  • ''
  • ()
  • []
  • {}
  • set()
  • frozenset()


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

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

疑問: if 文はいろんなことやりすぎじゃない?

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

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

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


答え: 反していません。Python の if 文は、あるか、ないかを判定するという1つの機能を有しています。


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

f:id:domodomodomo:20180805183340p:plain:w100

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

False と評価される値

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

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

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

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. int

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

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

3. object

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

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


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

class C(object):
    pass

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


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


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

class Dog(object):
    pass

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

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

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

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


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

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

このあと後半では、この2つの None の使い方について見ていきます。

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 を継承したのかについては、こちらに書きました。
Python のクラスオブジェクトとインスタンスオブジェクトってなに?


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
>>> 

疑問: どうやって、あるかないかを判定しているの?

答え: bool クラスをインスタンス化しています。

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

◯ __bool__ メソッド

また、適当な疑似コード見せて、本当にそうなの?って感じですが

f:id:domodomodomo:20180706114838p:plain:w200


以下のコードを実行してみてください。 if 文などで条件分岐をする際、論理演算を実行しています。内部で __bool__ メソッドが呼ばれています。

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

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

◯ メソッド探索の順番

__bool__ メソッドがない時は、__len__ メソッドが呼ばれます。 __len__ メソッドもない時は True が返されます。

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

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

assert not A()



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

assert not B()


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

assert C()

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











(後編)値が未定義であることを表すために使われる None


実は None には2つの使われ方があります。1つ目は、値が存在しないことを表すこと。これは上で見たとおりです。 2つ目は、値が未定義であること。2つ目の使われ方は、一般に「null安全」に関わる問題を引き起こします。 この2つ目の使われ方について、説明していきます。

未定義を表現するために使われる None

None には、オブジェクトが "存在しないこと" を表すだけでなく、未定義を表す時にも使われます。

a = None    # 本当は int が代入される。
lst = None  # 本当は list が代入される


例えば Effective Python 項目 20 「動的なデフォルト引数を指定するときには None とドキュメンテーション文字列を使う」では、 より具体的な例を示してくれています。

◯ 問題

ここで問題なのは、未定義を表すために None を使われると、 if 文が上手く動作しなくなってしまいます。

a = 0
if not a:
    print(a + 1)

a = None  # 未定義
if not a:
    print(a + 1)  # Error
lst = []
if not lst:
    lst.append('a')

lst = None  # 未定義
if not lst:
   lst.append('a')  # Error

◯ 解決策 その1

未定義であるかどうかを判定するコードを書きます。

a = None  # 未定義

if a is None:
    a = 0

if not a:
    print(a + 1)  # Error
lst = None  # 未定義

if lst is None:
    lst = []

if not lst:
   lst.append('a')  # Error


ポイント

  1. if not obj ではなく if obj is None
  2. if obj == None ではなく if obj is None


PEP 8 で if obj == None ではなく if obj is None と書くように定められています。
singleton を比較するときはに is を使う - Python の is と == の動作と違い

◯ 解決策 その2 None で未定義を表すよりは例外を返してしまう

例えば Python では 0 除算を行うと None ではなく例外を返します。

>>> 1 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 


実際、書籍 Effective Python でも、このようにして None に特別な意味を持たせることに対して否定的です。 "項目 14 Noneを返すよりも例外を発生させよう" を、ぜひご参考にしてください。

もちろんそうせざる得ない場面であったり、実際に Python の標準ライブラリでも、そのように None が使われているので、やっていけないわけではないのですが、なんとなく、なんとなく避けたほうが良いのかなと思ったりもします。

なぜなら未定義を表すために None を使うと null 安全に関わる問題が起こるからです。

◯ まとめ

  • 値が存在しないことを表す None と未定義を表すために使われている None を区別する。
  • 値が存在しないことを確認するためには if not obj 値が未定義であることを確認するには if obj is None を使う。

このことから、なにが分かったか?


if obj: という書き方は、
変数 obj に None が代入されないと
分かっている時にしか使えない。


x  # x にはリストか未定義を表す None が代入されます。

def f(x=None):
    if x is None:  # if not x とは書けない
        x = g()    # -> x に [] が代入されてるかもしれないから
    ...

プログラミングに関する推奨事項
Programming Recommendations - PEP 8

None のようなシングルトンと比較するときは is もしくは is not を使い決して == を使わないでください。

また if x is not None と書くことを意図して if x と書かないように気をつけてください。 -- 例えば、デフォルトとして None が設定されている変数または引数に、別の値が設定されているか確認するときです。 その別の値というのは、ブール演算においては偽と判定される可能性のあるコンテナ型をもつ値かもしれません。

Comparisons to singletons like None should always be done with is or is not, never the equality operators.

Also, beware of writing if x when you really mean if x is not None -- e.g. when testing whether a variable or argument that defaults to None was set to some other value. The other value might have a type (such as a container) that could be false in a boolean context!


これは実は null 安全という言葉と密接に絡んでいます。null というのは、Java や C 言語でよく使われる言葉ですが、Python に言い換えれば None です。「null 安全」を Python に言い換えれば「未定義であることを表すために None が変数や属性に代入されても問題なく動作する」という意味です。

null 安全とは何かについては、Java の記事になってしまいますが、この記事がわかりやすかったです。
null安全 とは Javaプログラマーが血と汗と涙を流さなくてすむ理由

null 安全を実現するために最近生まれた言語では、 変数に未定義を表す null がはいるかもしれない型と、はいらない型を分けているようです。 以下は Kotlin の例です。

// 普通のString型はnullを入れられない
var a: String = "abc"
a = null // !!コンパイルエラー
val l = a.length  // OK. aは絶対nullでないのでNPEは起こらない。
// NullableなString?型ならnullを入れられる
var b: String? = "abc"
b = null // ok
val l = b.length // !!コンパイルエラー: 変数 'b' は null がありうる

30分で覚えるKotlin文法 - Qiita


この記事は、様々な言語の null 安全について説明してくれています。
null安全でない言語は、もはやレガシー言語だ - Qiita

しかし、これは静的型付け言語での対策で。動的型付け言語である Python にはこのような解決策を取れません。 もし Python で null 安全を実現しようと思ったら、次のようなことをしていかないといけません。

def f(x):
    # x は None を許容しない
    # 例外を明示的に投げる
    if x is None:
        raise ValueError('x is not defined.')
    
    ...
def g(y):
    # y は None を許容する
    # 初期値を与える
    if y is None:
        y = 0
    
    ...


これは明確に動的言語の欠点だと思います。 静的言語であれば型の宣言時に ? をつけるだけで済みますが、 動的言語の場合、型を判別するための長いコードが必要になります。

動的言語しか使ったことのない人のコメントが面白い。 「テストで発見できるから大丈夫」という発想は「残業や休日出勤でカバーできるから大丈夫」と同じ。 属人性に頼らざるをえない動的言語はもはやブラック企業
http://b.hatena.ne.jp/entry/307053415/comment/megumin1


Effective Python の項目14, 20 は、さらっと書かれていますが、実際には null 安全について述べられていて、結構、重い内容だったという訳です。

◯ 思ったこと

いっそのこと None とは別に if 文で評価されたら Error を投げるような NotDefined っていう定数があればいいのかなとも思いました。

その方が PEP 20 の Simple is better than complex, Errors should never pass silently に適っているようにも感じます。

class NotDefinedClass(object):
    def __bool__(self):
        raise NotImplementedError

NotDefined = NotDefinedClass()


a = NotDefined

if a is NotDefined:
    pass

if a:  # raise NotImplementedError
    pass

Optional 型

とはいえ最近の Python では型を明示できるようになりました。 ところで変数に未定義の None が入る可能性が場合には、どうやって型を明示すればいいのでしょうか?

a: int
a = None  # mypy では error になる
$ mypy sample.py
sample.py:2: error: Incompatible types in assignment
(expression has type "None", variable has type "int")
$


Union を使うことによって、2つの型が代入される可能性があることを明示できます。

from typing import Union
b: Union[int, None]
b = None
$ # エラーにならない
$ mypy sample.py
$

typing.Union
ユニオン型; Union[X, Y] は X または Y を表します。


実は Optional という便利なものがあったりします。

from typing import Optional
c: Optional[int]
c = None
$ # エラーにならない
$ mypy sample.py
$

typing.Optional
Optional[X] は Union[X, None] と同値です。





◯ まとめ


if 文は

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