Pythonの「~True」はなぜFalseではなく、-2を返すのか? 3.16での廃止案を巡り活発化する議論:HPかわさきの研究ノート
Pythonではビット反転演算子の「~」をブール値に適用できます。その結果がどうなるか知っていましたか? そして、Python 3.16ではこの挙動が廃止される予定という話も知っていましたか? 今回はこのことについて調べてみました。
どうもHPかわさきです。
まだPython 3.15が開発途上であるにもかかわらず、今回はPython 3.16のお話ですが、少々お付き合いくださいませ。
ざっくりとまとめると?
- Pythonのboolはintのサブクラスで、Trueは1、Falseは0として振る舞うため、「~」演算子でビット反転できる
- その結果、「~True」は「-2」、「~False」は-1」となり、直感に反して「~True != False」になる
- この挙動はPython 3.12で非推奨になり、「~True」などが評価されるとDeprecationWarningが出る
- この挙動が廃止されるのはPython 3.14の予定だったが延期され、現状はPython 3.16での廃止が予定されている
- しかし、この廃止するかどうかを巡って、議論がまた活発化している
今回はこれをちょっとだけ掘ってみました。
ブール値のビット反転
そういうわけで、まずはTrueとFalseをビット反転させると、その結果がどうなるかを見ておきましょう。と、その前にTrueが1として、Falseが0として振る舞うことの確認からです。
t = True
f = False
print(f'{t} : {t & 0b1111:04b}')
print(f'{f}: {f & 0b1111:04b}')
ここではブール値と2進数の「1111」とのANDを取って、ブール値のビットパターンを4桁の2進数として表示しています。実行結果はもちろん次の通りです。
では、これらをビット反転させてみます(既に上の結果から想像は付きますが)。
t_neg = ~t
f_neg = ~f
print(f'{t}: negate: {t_neg}')
print(f'{t_neg} : {t_neg & 0b1111:04b}')
print(f'{f}: negate: {f_neg}')
print(f'{f_neg} : {f_neg & 0b1111:04b}')
実行結果を以下に示します。
何やらごにょごにょとメッセージが表示されています。これが核心のメッセージなのですが、ここでは一番下の出力を見てください。上のコードでTrueをビット反転させた結果は「1110」となりました(これは反転結果の下位4bitだけを表示するために 0b1111 とANDした結果です)。この「1110」は、4 bitの2の補数表現では「-2」に対応します。同様にFalseをビット反転させると「-1」になります。
Pythonの整数は固定ビット幅ではなく、ビット演算の結果としては、負数は概念的に無限に1が続く2の補数表現として振る舞うので、マスクするとこう見えるというお話です。
さて、「-1」は真として評価されるので「~False」は真となります(bool関数でこれをブール値にすると「bool(~False) == True」となるのであって、「~False == True」ではない点には注意)。一方、「1」をビット反転させると「-2」です。これもまた真として評価されるので、これをブール型の値として考えると(bool(~True) )、これもまた真です。つまり、「~True != False」となります。これって直感に反しているような気がしませんか?
Pythonにおける「~演算子」はビット反転演算子であって、論理否定演算子ではないというのがポイントです。そのため、今見た挙動は問題ないと言語で定められているということです。
そして、表示されていた「DeprecationWarning」という警告。これについて次に考えてみましょう。
ブール値のビット反転は非推奨
今見たような挙動については実は2019年にissueが上げられています(bool(~True) == True)。そして、これを基に2023年の4月にPull Requestが立てられました。このPull Requestはマージされて、Python 3.12からはDeprecationWarningが表示されるようになりました。
このPull Requestがマージされた時点では、ブール値のビット反転はPython 3.14で廃止される予定だったようです。
しかし、2024年の8月に「Extend deprecation warning period for bitwise inversion on bool type」というissueが立てられ、最終的に非推奨期間を2年延期するPull Requestが作られてマージされたようです。この結果、ブール値のビット反転が廃止されるのはPython 3.16になったというわけです(これは2026年2月の時点でも変わっていません)。
実は同時に「Prohibit bitwise inversion on bool type」というPull Requestが立てられています。これはPython 3.14でブール値のビット反転を廃止するためのものでした。しかし、このPull Requestには、上で紹介した「ブール値のビット反転の非推奨期間を延長する」というissueがリンクされていました。これら2つのPull Requestとissueを立てたのが同じ人物であることを考えると、当初から廃止についての議論を促す目的があったのかもしれませんね。実際、「~bool deprecation」でこのPull Requestをきっかけに議論が行われたようです。そして結局、このPull RequestはPython 3.14のソースコードにマージされることなくクローズされ、非推奨期間が2年延長されることになったのでした。
そして、時は流れ、Python 3.15のα6もリリースされ、いよいよPython 3.16の開発にも力が入ろうというタイミングでこの議論が再度行われ始めたようです。
その発端となったのが「Real world impediments through ~bool deprecation」です。これは「ブール値のビット反転を廃止した場合に、現実に問題に直面する例があれば教えてほしい」というもの。そして、上でも紹介した「~bool deprecation」では、議論が再開されています(「Real world ……」よりも「~bool ……」の方が活発ではあります)。
「活発」と書きましたが、活発なのかな(笑)。でも、Pythonの生みの親でもあるGuido van Rossum氏も発言しています。筆者は注目していますよ。
ブール値のビット反転を廃止することにはどんな問題が?
ブール値のビット反転の廃止については、もちろん、支持する人と支持しない人がいます。
- 支持:「~True」が「-2」になるのは直感的ではない。誤用されやすい(Trueの論理否定を求めようとして「~」を使ってしまう)。そうした誤用(バグ)が見つけにくく、型チェックをすり抜ける可能性がある。Trueの論理否定を求めるなら「not」やビット反転したいのであれば「~int(x)」を使うべき
- 支持しない:意図して「~」を使う場面がある。既存のコードを壊す。boolがintである以上、intを使える場面でboolが使われるのは当たり前(廃止すると「親クラスが使用できる場面には子クラスは問題なく使えなければいけない」というリスコフの置換原則に反する)。ブール値に対する「~」を廃止して、他の演算子(「-」「+」「*」など)はそのままというのはヘン。Pythonは成熟し広く使われているので、目に見える挙動を変更するリスクは取るべきではない
この他にNumPyでは「~」は論理否定としても使われるので、一貫性を持たせるべきのような意見もありました。
Guido van Rossum氏は当初、非推奨→廃止という流れに賛成していたのですが、現在では意見を変えて、非推奨としたのは取り消すべきという立場に立っています(ただし、ブール値を反転しようとしているコードにリンターや型チェッカーが何らかの警告を発生させることには賛成できるとのこと)。
ちなみにJavaScriptもtrueをビット反転すると「-2」になります。C(stdbool.hをインクルード)やC++のtrueも多くの環境では同様でしょう(Web上のプレイグラウンドで試しただけですが)。そういう意味では、この挙動はPythonに限ったものではありません。その一方で、ブール値に対するビット反転を許可していない言語もあります(例えば、Rubyなど)。
なんてことを考えてみると、いったんは廃止が決まったブール値のビット反転ですが、廃止が取り消されるかもしれませんし、やっぱり廃止になるかもしれませんね(どっちなんだよ)。
Pythonのこうした挙動は、bool型がint型を継承していることによるものであり、このような挙動を許すか許さないかはPythonが生まれてから間もない状況であれば、変更もできたでしょう。しかし、ここまで広く使われている状況でこの非推奨から廃止という変更をできるかどうかはまだ分かりません。注目してみたいと思います。
Copyright© Digital Advantage Corp. All Rights Reserved.



