2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Python】ゼロだけど空じゃないオブジェクトは Truthy か Falsy か?

Last updated at Posted at 2019-12-11

Python における Truthy と Falsy について気になることがあったので調べつつ、Truthy と Falsy に関する雑談をする記事になります。 表題だけが気になる方はここから飛んでください。

Truthy と Falsy に関する雑談

Truthy / Falsy とは

Truthy および Falsy という言い回しは JavaScript で登場するものですが、その概念そのものは多くの言語に登場するため、ここでは一般化して使用いたします。

Truthy とは、論理評価すると真として扱われる値を示す形容詞です。 Falsy とは、論理評価すると偽として扱われる値を示す形容詞です。

人によっては、論理評価すると真 / 偽として扱われる値のうち、「代表値」を除いたもののみを Truthy / Falsy とよぶ場合もあるかもしれませんが、ここでは JavaScript における定義にしたがいすべて Truthy および Falsy とします。

Truthy / Falsy に対するスタンス

言語の Truthy / Falsy に対するスタンスは、個人的にですが以下の3パターンに分けられると思います。 本稿ではパターン A を「純真偽性」、パターン B とパターン C を「拡張真偽性」と呼び分けることにします。

パターン A: Truthy と Falsy はそれぞれ一つだけ

このタイプの言語はおおよそ真理値専用の型を持ち、そのとりうる値を 2 種類に制限しています。 あるいは、シングルトンなクラスとして「真専用のオブジェクト」「偽専用のオブジェクト」を持つ場合もあります。 それら以外の値を論理評価しようとするとエラーになります。

代表例として Java が挙げられるでしょう。 ただし、Java の場合プリミティブな真理値とラップされた真理値があるので厳密にはそうでないと言えなくもないです。

パターン B: すべての値は Truthy か Falsy のどちらかである

このタイプの言語ではあらゆる値を論理評価できます。 場合によっては真理値専用の型を持たない場合すらありえるかもしれません。 ただし、比較演算や論理否定の返り値として「代表値」の存在は要請されます。

代表例として JavaScript が挙げられるでしょう。 だから Java と JavaScript は違うって言ってんじゃねーかよ

パターン C: Truthy / Falsy が複数あるが、どちらでもない値もある

個人的にはどっちつかずでいい印象を持ちません。 ただ、その「例外」がnullとかだった場合はある意味妥当性がある設計かもしれません(後述)。

C 言語は仕様上これです。 というのも C 言語は論理演算などを整数型を使用して行っていて、0以外をすべて Truthy とみなしているためです。 逆に整数型以外は論理評価できません。

拡張真偽性言語のメリット / デメリット

拡張真偽性言語にはいくつかのメリットがあります。

一つは条件式を簡略化できることです。

例えば、ゼロ除算を避ける際には以下のようなコードを書くかもしれません。

if (b !== 0) {
    console.log(a / b)
} else {
    console.log('ゼロでは割れません。')
}

しかし、0は Falsy ですから、

if (b) {
    console.log(a / b)
} else {
    console.log('ゼロでは割れません。')
}

で十分なことがわかります。

もう一つは一部の論理演算を一般化できることです。

たとえば、論理和は古典的な論理学では「左辺と右辺がどちらも偽であるとき偽、それ以外は真を返す」という演算ですが、これを「左辺が Truthy のとき左辺を、左辺が Falsy のとき右辺を返す」と解釈することで既存の真理値表を変更せずに拡張できます。

これを利用して一部のロジックを簡約化できます。 たとえば、入力がない(空文字列の)場合はデフォルトの文字列を入れるというようなロジックは、素直に書こうとすると以下のようになります。

if (!name) {
    name = 'ゲスト';
}

/* あるいは三項演算子を使用して */
name = name ? 'ゲスト' : name

しかし、空文字列が Falsy であることを利用すると、一般化された論理和を使って以下のように書けます。

name = name || 'ゲスト'

なぜなら、nameが Truthy だった場合(つまり空文字列以外だった場合)そのまま左辺が返され、Falsy だった場合(つまり空文字列だった場合)右辺が返されるからです。

しかしこれらは同時にデメリットもはらんでいます。

たとえば前者ですが、実はbに入っているのが数値である保証がありません。 数値でない Truthy な値が入った場合、条件式をすり抜けて実行時エラーを発生させます。

こういった落とし穴は動的型付けな言語で顕著ですが、静的型付けでも Null だけは例外的に代入できる(いわゆる Null 安全でない)言語は多く、Null も論理評価できてしまうと予期せぬ動作が発生しえます。

むしろ、動的型付け言語を使う人は型の整合性を自分でハンドリングする気概でやっている(そして型に関する実行時エラーがよく起こるのでもう慣れっこ)と思うので、コンパイル任せの静的型付け言語を使っている人ほど危ない可能性もありますね。

また、この仕様はかの悪名高きヨーダ記法を生み出す片親になりました。 もう片方の親は「代入演算子が値を返す仕様」です。 結果、

if (a == 5) {
    /* ... */
}

と書いたつもりが

if (a = 5) {
    /* ... */
}

となっていても、

  1. a = 5は(aの値がなんであろうと)値5を返す
  2. 5は Truthy である
  3. if ブロックの中身実行!

となり、バグの温床を生み出しました。 これを回避するために

if (5 == a) {
    /* ... */
}

と書くヘンテコな風習が生まれたわけです。

これが組織のレガシーなコーディングルールに残り、必要のない状況でも(つまり純真偽性言語や代入演算子が値を返さない言語、あるいは if 条件式の中で代入を行っていると警告を出すコンパイラを使用しているとしても)強要される、という状況を生み出しています。 必要があって生まれた風習がやがて意味を忘れ去られ、形だけの無意味な「マナー」になっていく……現実世界でもしばしばあることではありますが……。

閑話休題。 このように Truthy / Falsy のスタンスはメリット(効率化)とデメリット(落とし穴)が背中合わせですから、業務で初めて触れる言語を使うことになったときなどは、純真偽性言語なのか拡張真偽性言語なのかはしっかり意識しておくことが大事です。 あとで「知らなかった そんなの……」となると辛いですよ(経験談)。

Python における Truthy / Falsy

なにが Falsy を決めるのか

さて、やっと表題です。 ここからしばらく Python2 の話になります。 3 の場合は最後で触れます。

Python は拡張真偽性言語に属します。 Falsy でない値はすべて Truthy です。 一般的な値の中では、以下のような値などが Falsy となっています。

  • False
  • 0
  • 0.0
  • 空のリスト
  • 空の辞書
  • 空の集合
  • 空のタプル
  • None

ところで、Python における bool 型は実は int 型のサブクラスであり、TrueFalseは実はそれぞれ10と等価です。 これを踏まえると、

  • 数値でいうとゼロ
  • 空のコレクション
  • None

と一般化できそうですよね?

特殊な値であるNoneはともかくとして、Python には「ゼロかどうか」「コレクションの長さ」を示す特殊メソッドが定義されています。 つまり、これらを実装することで独自のオブジェクトが Truthy か Falsy かをコントロールできるということです。

早速試してみましょう。 まずはどちらも実装しない場合です。

class MyObjA:
    def __init__(self):
        pass


my_obj = MyObjA()
print('Truthy' if my_obj else 'Falsy')
Truthy

Falsy である要素がないので、Truthy と扱われているのがわかります。

つづいて、「数値でいうとゼロ」ということにしてしまいましょう。 __nonzero__メソッドを実装します。

class MyObjB:
    def __init__(self, nz):
        self.nz = nz

    def __nonzero__(self):
        return self.nz


my_obj = MyObjB(True)
print('Truthy' if my_obj else 'Falsy')
my_obj = MyObjB(False)
print('Truthy' if my_obj else 'Falsy')
Truthy
Falsy

ご覧の通り、__nonzero__Falseを返す(正確に言えば Falsy な int 型の値を返す)場合はオブジェクト自体が Falsy と扱われているのがわかります。

つづいて「空のコレクション」もやってみましょう。 __len__メソッドを実装します。

class MyObjC:
    def __init__(self, ln):
        self.ln = ln

    def __len__(self):
        return self.ln


my_obj = MyObjC(10)
print('Truthy' if my_obj else 'Falsy')
my_obj = MyObjC(0)
print('Truthy' if my_obj else 'Falsy')
Truthy
Falsy

ご覧の通り、__len__0を返す(正確に言えば Falsy な int 型の値を返す)場合はオブジェクト自体が Falsy と扱われているのがわかります。

矛盾する場合

しかし、これらの条件が互いに矛盾している場合はどうなるのでしょう? これが表題の意です。

実際に試してみると……

class MyObjD:
    def __init__(self, nz, ln):
        self.nz = nz
        self.ln = ln

    def __nonzero__(self):
        return self.nz

    def __len__(self):
        return self.ln


my_obj = MyObjD(True, 10)
print('Truthy' if my_obj else "Falsy")

my_obj = MyObjD(False, 0)
print('Truthy' if my_obj else "Falsy")

my_obj = MyObjD(True, 0)
print('Truthy' if my_obj else "Falsy")

my_obj = MyObjD(False, 10)
print('Truthy' if my_obj else "Falsy")
Truthy
Falsy
Truthy
Falsy

と、このように長さに関わらず「ゼロか否か」が優先されているのがわかります。 この理由は、__len__で不正な値を返したときのエラーメッセージから読み取れます。

class MyObjC:
    def __init__(self, ln):
        self.ln = ln

    def __len__(self):
        return self.ln


my_obj = MyObjC(0.0)
print('Truthy' if my_obj else 'Falsy')
Traceback (most recent call last):
  File "Main.py", line 10, in <module>
    print('Truthy' if my_obj else 'Falsy')
TypeError: __nonzero__ should return an int

__nonzero__は int 型を返すべきです」と怒られました。 つまり、デフォルトの __nonzero__実装は__len__を呼び出していると推定できます。 確かめてみましょう。 __nonzero__は組み込み関数boolで呼ばれます。

class MyObjE:
    def __init__(self, ln):
        self.ln = ln

    def __len__(self):
        print('__len__ called!')
        return self.ln


my_obj = MyObjE(0)
b = bool(my_obj)
print(b)
print(type(b))
__len__ called!
False
<type 'bool'>

推測どおりです! デフォルトの__nonzero__ 実装が__len__を呼び出しているということは、MyObjC で__len__のチェックに見えていたものは実際には__nonzero__ を呼んでいただけ……つまり両方を実装したとしても、見ているのは__nonzero__ の方だけなのです。 だから、「ゼロだけど空じゃないオブジェクト」は Falsy となるわけですね。

Python3 において

ちなみに、特殊メソッド名__nonzero__と組み込み関数名boolの名前の不一致は Python3 で是正され、__bool__になりました。 故にこうなります。

class MyObjD:
    def __init__(self, nz, ln):
        self.nz = nz
        self.ln = ln

    def __nonzero__(self):
        return self.nz

    def __len__(self):
        return self.ln


my_obj = MyObjD(True, 10)
print('Truthy' if my_obj else "Falsy")

my_obj = MyObjD(False, 0)
print('Truthy' if my_obj else "Falsy")

my_obj = MyObjD(True, 0)
print('Truthy' if my_obj else "Falsy")

my_obj = MyObjD(False, 10)
print('Truthy' if my_obj else "Falsy")
Truthy
Falsy
Falsy
Truthy
class MyObjF:
    def __init__(self, nz, ln):
        self.nz = nz
        self.ln = ln

    def __bool__(self):
        return self.nz

    def __len__(self):
        return self.ln


my_obj = MyObjF(True, 10)
print('Truthy' if my_obj else "Falsy")

my_obj = MyObjF(False, 0)
print('Truthy' if my_obj else "Falsy")

my_obj = MyObjF(True, 0)
print('Truthy' if my_obj else "Falsy")

my_obj = MyObjF(False, 10)
print('Truthy' if my_obj else "Falsy")
Truthy
Falsy
Truthy
Falsy

そして、__bool__はその名の通り bool 型の返り値しか許さなくなりました。

class MyObjF:
    def __init__(self, nz, ln):
        self.nz = nz
        self.ln = ln

    def __bool__(self):
        return self.nz

    def __len__(self):
        return self.ln


my_obj = MyObjF(1, 10)
print('Truthy' if my_obj else "Falsy")
Traceback (most recent call last):
  File "Main.py", line 14, in <module>
    print('Truthy' if my_obj else "Falsy")
TypeError: __bool__ should return bool, returned int

__bool__が bool 型しか許さなくなりましたが、デフォルト実装が__len__を呼ぶ仕様はどうなったでしょうか?

class MyObjE:
    def __init__(self, ln):
        self.ln = ln

    def __len__(self):
        print('__len__ called!')
        return self.ln


my_obj = MyObjE(0)
b = bool(my_obj)
print(b)
print(type(b))
__len__ called!
False
<class 'bool'>

デフォルトの__bool__実装が__len__を呼ぶ仕様に変更はないようです。 もとから__len__の返り値を bool 型にキャストしているから問題ないようですね。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?