こんな人にオススメ
pythonで数値にチルダ~
をつけてみると出力が-((数値)+1)
になって返ってくる!例えば
$\sim3 = -4$
これはいったいなんなの?どうなっているの?
ということで、今回はpythonとチルダ~
の関係について解説する。後に書くがnumpy
のbool
型と~
の関係が便利なもので、~
をよく使っている中で今回の疑問ができてきた。
実際に調べてみると、「~(数値) = -((数値)+ 1)
になります」とだけ書いてある記事が多かった。ので、本記事でざっくりとどんなことが起きているのかを解説する。
執筆者自身としては本記事の内容でモヤモヤがスッキリしたので、是非とも参考にしていただきたい。
python環境は以下。
- Python 3.9.4
- numpy 1.20.3
ざっくり概要
最初に本記事の概要を述べると、数値に~
をつけた場合は以下の手順で処理が行われている。
- 数値を2進数に変換(
3
:0011
) - 変換した2進数表記のビット反転を計算
- 10進数:
3
- 2進数:
0011
- ビット反転:
1100
- 10進数:
- ビット反転後の数値の補数を計算
1100
→0011
→0011 + 0001
=0100
- 補数と元の数の感形式より処理結果を出力
補数の計算なので、以下の式が成り立つ(今は4 bit計算)。
$1100\ +\ 0100\ =\ 0000$
元の数3
のビット変換後の数値は1100
なので、この式より
$1100\ =\ -0100$
-0100
は-4
に相当するので、結果、3
のビット反転はpython上では-4
として出力される。
print(~3) # -4
ことの発端
tpl = (-2, -1, 0, 1, 2, 3, 7, 8, 10, 100) print(tpl) # (-2, -1, 0, 1, 2, 3, 7, 8, 10, 100) for val in tpl: inversion = ~val print(f"{val}: {inversion}") # -2: 1 # -1: 0 # 0: -1 # 1: -2 # 2: -3 # 3: -4 # 7: -8 # 8: -9 # 10: -11 # 100: -101
まずはことの発端。実際には異なるコードだったが、イメージは上のコードのようなもの。数値に~
をつけると値がおかしなことになる。ものの見事に-((数値) + 1)
の値が出力されている。
自称に書くbool
との関係を使用しているときにふと数値ではどうなるのだろうということで試した。
ちなみにval
をint
ではなくfloat
にするとエラー。
# for val in tpl: # val = float(val) # inversion = ~val # print(f"{val}: {inversion}") # # inversion = ~val # # TypeError: bad operand type for unary ~: 'float'
チルダ(~
)を適用
では、そもそもチルダ~
はどんなときに使えるのかについて解説する。以下に書く内容以外にも使用用途はあると思うが、今回に関連しているものだけを取り上げた。
bool
に適用
ということで先ほどから書いているbool
から適用する。がしかし、通常のbool
だと便利な処理の恩恵に預かることができない。
便利な処理の恩恵は次節にて解説する。ここではbool
の型とかについても触れる。
print(type(True)) # <class 'bool'> print(type(False)) # <class 'bool'>
実はbool
型はint
のサブクラスで、int
型に変換するとそれぞれTrue
が1
に、False
が0
に相当する。
# boolは実はintのサブクラスで、Trueが1、Falseが0に相当 print(int(True)) # 1 print(int(False)) # 0
ということでこの状態で~
を使うと先ほど同様、数値に換算されるだけ。
tf = (True, False) print(tf, type(tf)) # (True, False) <class 'tuple'> for val in tf: print(f"{val}({type(val)}): {~val}") # True(<class 'bool'>): -2 # False(<class 'bool'>): -1
なお、tf
のまま~
をつけるとエラー。
# print(~tf, type(~tf)) # print(~tf, type(~tf)) # TypeError: bad operand type for unary ~: 'tuple'
numpy
のbool
にすると真偽の反転
bool
型で便利でないならどうすればいいのか。実はnumpy
のbool
に変換することでその恩恵に預かることができる。型はnumpy.bool_
で、int
にするとbool
型同様、数値として出力される。
import numpy as np tf = np.array([True, False]) print(tf, type(tf)) # [ True False] <class 'numpy.ndarray'> for val in tf: int_vl = int(val) print(f"{val}({type(val)}): {int_vl}({type(int_vl)})") # True(<class 'numpy.bool_'>): 1(<class 'int'>) # False(<class 'numpy.bool_'>): 0(<class 'int'>)
しかし、このnumpy.bool_
に~
を適用するとその真偽が逆転する。これがすごい便利だと感じた。わざわざ2つの真偽を用意しなくても、1つの真偽でどちらも賄うことができる。
さらに、配列の初めにつけることで、配列全ての真偽を逆転させることもできる。これは画期的。
# numpy配列にするとboolのチルダは真偽の反転 for val in tf: print(f"{val}({type(val)}): {~val}") # True(<class 'numpy.bool_'>): False # False(<class 'numpy.bool_'>): True print(~tf, type(~tf)) # [False True] <class 'numpy.ndarray'>
np.isnan
の応用にも使える
ではどんなときにnumpy.bool_
の真偽逆転が使えるかというと、執筆者が一番使用しているシーンはnp.isnan
の使用時。
np.isnan
は非数(Not a Number)であればTrue
をそうでなければFalse
を返すという関数。シンプルに使用するとNaN
の部分がTrue
になるので、スライスに使用する際にはNaN
だけ抽出してしまう。
しかし、~
を使用することで真偽が逆転し、NaN
ではない部分だけを抽出することが可能になる。
arr = np.array([1, np.nan, 10., np.nan]) print(arr) # [ 1. nan 10. nan] is_nan = np.isnan(arr) print(is_nan) # [False True False True] print(~is_nan) # [ True False True False] # スライスで要素抽出 print(arr[is_nan]) # [nan nan] print(arr[~is_nan]) # [ 1. 10.]
もし~
を使用しなかった場合は、例えば以下に示すようにnp.where
で真・偽の時のそれぞれの扱いを設定しなければならない。
# ~を使用せずに真偽の反転を行うのは面倒 is_nan2 = np.where(np.isnan(arr), False, True) print(is_nan2) # [ True False True False]
2進数のビット反転
ではここから本題のビット反転と補数について解説する。日常生活では使用しないものだが、軽くでも知っていただけると知識として優良だろう。
2進数
コンピュータが0
と1
で動いているということは聞いたことがあるだろう。この0
と1
の2種類の数値のみで様々な数値を表現するのが2進数。他にも8進数や16進数が有名ではあるが、ここでは割愛する。
2進数の表現は以下のように0
と1
を並べて表示する(4桁ずつ半角スペースを開けることもある。今回は簡単のために4桁だけにする)。
10進数(いつもの数値) | 2進数 |
0 | 0000 |
1 | 0001 |
2 | 0010 |
3 | 0011 |
7 | 0111 |
8 | 1000 |
10 | 1010 |
15 | 1111 |
10進数に戻すには、一番左の数値が1
かどうか、それよりも右に行くにつれて2
かどうか、4
かどうかのように2倍ずつされて計算される。数値が1
だと1 ×(1の存在数位置に該当する値: 2とか4とか)
で計算できる。
- 10進数の
2
は2進数では0010
: $0\times8+0\times4+1\times2+0\times1=2$ - 10進数の
7
は2進数では0111
: $0\times8+1\times4+1\times2+1\times1=7$ - 10進数の
15
は2進数では1111
: $1\times8+1\times4+1\times2+1\times1=15$
なかなかイメージしにくいかもしれないので、自分なりに理解の仕方を模索していただきたい。
ビット反転
続いてはビット反転。ビット反転はとてもシンプルで、2進数の0
と1
と逆転させる、すなわち0
を1
に、1
を0
にするという操作。
10進数(いつもの数値) | 2進数 | ビット反転 |
0 | 0000 | 1111 |
1 | 0001 | 1110 |
2 | 0010 | 1101 |
3 | 0011 | 1100 |
7 | 0111 | 1000 |
8 | 1000 | 0111 |
10 | 1010 | 0101 |
15 | 1111 | 0000 |
2の補数
ここまでで必要となる基本的な2進数のお話は終了。ここからが少し面倒なところになるが補数の話に入る。わかりやすく書こうと思うので、どうかついてきていただきたい。
なお、1の補数やその他の補数も存在するが、ここでは2の補数が使われるので2の補数だけを取り上げる。
2進数(0
と1
)だけで負の数を表す
我々が普段から使用している数字は10進数で、引き算ももちろん計算可能である。
$10\ -\ 7\ =\ 3$
しかし、2進数では0
と1
の足し算という情報しか存在していないので、このような引き算を表現することができない。そこで使用される考え方、操作が「補数」というもの。イメージは以下。
$10\ +\ (-7)\ =\ 3$
どういうことかというと、7
を「引く」という操作を-7
を「足す」と書き換えた。そう、2進数では足し算は使用できるので、引き算を足し算にしようという考え方。
では、-7
というものをどうやって表現するのか。次節より解説する。
2の補数の作成手順
2の補数の役割は負の数を表現するというもの。だとすれば補数の-1
倍、すなわち正の数にした場合との足し算が0
になることは明確。
$(-7)\ +\ 7\ =\ 0$
この時の-7
という値が7
の補数となる。
10進数の7の2進数表現は0111
だった。これを0にしたい場合はどうすればいいのか。
ズバリ、ピッタリ一桁あげればいい。
どういうことかというと、上の10進数の(-7)+7=0
の足し算を2進数にしたときに0
にすればいいということ。
$(7の補数)\ +\ 0111\ = \ 0000$
そのためには現在の4 bit表現(4桁ってこと)から1桁はみ出て10000
にする。さすれば、4 bitで表現すると一番左の1
は切られて0000
になる。この条件に当てはまる-7
の2の補数は1000
になる。
$1000\ +\ 0111\ =\ 0000$
ちなみに2進数では以下の計算ルールがある。
- $0\ +\ 0\ =\ 00$
- $1\ +\ 0\ =\ 01$
- $0\ +\ 1\ =\ 01$
- $1\ +\ 1\ =\ 10$
これに従うと、上記の足し算の結果は10000
になることがわかると思う。この1000
という値が0111
、すなわち7
の補数というわけだ。
ちなみに簡単な2の補数の作成方法は以下。
- ビット反転させる
1
を足す(0001
を足す)
数値にチルダをつけると-(値+1)
になる問題
では最後に、本記事の問題点である数値にチルダ~をつけると-(値+1)
になる問題の解説だ。
処理の概要
ではどうしてこれらの操作が必要になるのかということだ。実はpythonではビット反転の時に、無限に続く上位の桁まで反転してしまうという特徴がある。すなわち、上記の例だと0111
をビット反転するとシンプルに1000
にはならない。
python上では0111
は
$...000\ 0111$
というように左側に0
が無限に続いているのだ。これをビット反転してしまうと
$...111\ 1000$
と無限に左側に1
が続くことになる。そうすると、ビット反転後の値は無限∞となってしまい、計算上正しくない結果になる。
そこで補数の登場だ。この...111 1000
の補数を計算し、これを代わりにビット反転の値として表すということ。前章の式風に書くと
$(ビット反転後の数値)\ +\ (ビット反転後の数値の補数)\ =\ 0$
これはすなわち
$(ビット反転後の数値)\ =\ -(ビット反転後の数値の補数)$
だから、無限に1
が続くビット反転後の数値を出力するのではなく、その補数で便宜上表現するということ。
確認コード
ということで、上記までの処理をまとめたコードを示す。出力内容としては左2が10進数表記、右2が2進数表記だ。ビット反転ではなく、ビット反転の補数になっていることがわかるだろう。
なお、-001
とかの数字が3桁しかない場合は適宜-0001
などのように0を埋めてほしい。
tpl = (-2, -1, 0, 1, 2, 3, 7, 8, 10, 100) print('10進数: ~数値, 2進数: ~数値') for val in tpl: inversion = ~val val_f = format(val, '04b') inversion_f = format(inversion, '04b') print(f"{val}: {inversion}, {val_f}: {inversion_f}") # 10進数: ~数値, 2進数: ~数値 # -2: 1, -010: 0001 # -1: 0, -001: 0000 # 0: -1, 0000: -001 # 1: -2, 0001: -010 # 2: -3, 0010: -011 # 3: -4, 0011: -100 # 7: -8, 0111: -1000 # 8: -9, 1000: -1001 # 10: -11, 1010: -1011 # 100: -101, 1100100: -1100101 # ビット反転→補数をとった一番右の2進数で補数を取れば、ビット反転したことを確認できる
ざっくりまとめ
最後に冒頭の手順を再度掲載する。
- 数値を2進数に変換(
3
:0011
) - 変換した2進数表記のビット反転を計算
- 10進数:
3
- 2進数:
0011
- ビット反転:
1100
- 10進数:
- ビット反転後の数値の補数を計算
1100
→0011
→0011 + 0001
=0100
- 補数と元の数の感形式より処理結果を出力
補数の計算なので、以下の式が成り立つ(今は4 bit計算)。
$1100\ +\ 0100\ =\ 0000$
元の数3
のビット変換後の数値は1100
なので、この式より
$1100\ =\ -0100$
-0100
は-4
に相当するので、結果、3
のビット反転はpython上では-4
として出力される。
print(~3) # -4
ひょんなことから知識を得る
今回はpythonにおいて数値に~をつけると出力が-(数値 + 1)
として出力される問題について解説した。ふと気になって調べてみると、色々とわかって興味深いと感じた。
こんな感じでとりあえずチャレンジしてみることで新しい知識とかを得るきっかけになるのでこれからも色々試してみたい。
関連記事
-
-
【python3&四捨五入】四捨五入すると1.5も2.5も2.0になる問題
続きを見る
-
-
【python&初級】のlistとかforとかifとかまとめ
続きを見る
-
-
【python&Excel】pandasでエクセルファイルを読み込み・書き出し
続きを見る
-
-
【max(), sorted()&key】max関数とかで使うkeyを活用
続きを見る