カテゴリー

当サイトはアフィリエイトプログラムによる収益を得ています〈景品表示法に基づく表記です)

Python基礎

【python&~】数値にチルダ(~)をつけると値が+1されて負の数になる(ビット反転)

2021年6月5日

こんな人にオススメ

pythonで数値にチルダ~をつけてみると出力が-((数値)+1)になって返ってくる!例えば

$\sim3 = -4$

これはいったいなんなの?どうなっているの?

ということで、今回はpythonとチルダ~の関係について解説する。後に書くがnumpybool型と~の関係が便利なもので、~をよく使っている中で今回の疑問ができてきた。

実際に調べてみると、「~(数値) = -((数値)+ 1)になります」とだけ書いてある記事が多かった。ので、本記事でざっくりとどんなことが起きているのかを解説する。

執筆者自身としては本記事の内容でモヤモヤがスッキリしたので、是非とも参考にしていただきたい。

python環境は以下。

  • Python 3.9.4
  • numpy 1.20.3

運営者のメガネです。YouTubeTwitterInstagram、自己紹介はこちら、お問い合わせはこちらから。

運営者メガネ

ざっくり概要

最初に本記事の概要を述べると、数値に~をつけた場合は以下の手順で処理が行われている。

  1. 数値を2進数に変換(3: 0011)
  2. 変換した2進数表記のビット反転を計算
    • 10進数: 3
    • 2進数: 0011
    • ビット反転: 1100
  3. ビット反転後の数値の補数を計算
    • 110000110011 + 0001=0100
  4. 補数と元の数の感形式より処理結果を出力

補数の計算なので、以下の式が成り立つ(今は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との関係を使用しているときにふと数値ではどうなるのだろうということで試した。

ちなみにvalintではなく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型に変換するとそれぞれTrue1に、False0に相当する。

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

numpyboolにすると真偽の反転

bool型で便利でないならどうすればいいのか。実はnumpyboolに変換することでその恩恵に預かることができる。型は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進数

コンピュータが01で動いているということは聞いたことがあるだろう。この01の2種類の数値のみで様々な数値を表現するのが2進数。他にも8進数や16進数が有名ではあるが、ここでは割愛する。

2進数の表現は以下のように01を並べて表示する(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進数の01と逆転させる、すなわち01に、10にするという操作。

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進数(01)だけで負の数を表す

我々が普段から使用している数字は10進数で、引き算ももちろん計算可能である。

$10\ -\ 7\ =\ 3$

しかし、2進数では01の足し算という情報しか存在していないので、このような引き算を表現することができない。そこで使用される考え方、操作が「補数」というもの。イメージは以下。

$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. ビット反転させる
  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進数で補数を取れば、ビット反転したことを確認できる

ざっくりまとめ

最後に冒頭の手順を再度掲載する。

  1. 数値を2進数に変換(3: 0011)
  2. 変換した2進数表記のビット反転を計算
    • 10進数: 3
    • 2進数: 0011
    • ビット反転: 1100
  3. ビット反転後の数値の補数を計算
    • 110000110011 + 0001=0100
  4. 補数と元の数の感形式より処理結果を出力

補数の計算なので、以下の式が成り立つ(今は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を活用

続きを見る

ガジェット

2023/9/18

【デスクツアー2022下半期】モノは少なく、でも効率的に Desk Updating #0

今回はガジェットブロガーなのにデスク環境を構築していない執筆者の ...

ライフハック

2023/9/16

【Audible vs YouTube Premium】耳で聴く音声学習コンテンツを比較

ワイヤレスイヤホンが普及し耳で学習することへのハードルが格段に下 ...

完全ワイヤレスイヤホン(TWS)

2023/9/18

【SENNHEISER MOMENTUM True Wireless 3レビュー】全てが整ったイヤホン

今回は高音質・高機能なSENNHEISERのフラグシップ完全ワイヤレスイヤホン「SENNH ...

ライフハック

2023/3/11

【YouTube Premiumとは】メリットしかないから全員入れ

今回はYouTube Premiumを実際に使ってみてどうなのか、どんなメリット/デメリット ...

マウス

2023/8/17

【Logicool MX ERGOレビュー】疲れない作業効率重視トラックボールマウス

こんな人におすすめ トラックボールマウスの王道Logicool MX ERGOが気になるけどऩ ...

ベストバイ

2023/9/18

【ベストバイ2022】今年買って良かったモノのトップ10

2022年ベストバイ この1年を振り返って執筆者は何を買ったのか。ガジェッ& ...

スマホ

2023/1/15

【楽天モバイル×povo2.0の併用】月1,000円の保険付きデュアルSIM運用

こんな人におすすめ 楽天モバイルとpovo2.0のデュアルSIM運用って実際のとこ ...

マウス

2023/9/16

【Logicool MX ERGO vs MX Master 3】ERGOをメインにした決定的な理由

こんな疑問・お悩みを持っている人におすすめ 執筆者はLogicoolのハイエンӠ ...

macOSアプリケーション

2022/9/30

【Chrome拡張機能】便利で効率的に作業できるおすすめの拡張機能を18個紹介する

こんな人におすすめ Chromeの拡張機能を入れたいけど、調べても同じような ...

macOSアプリケーション

2023/5/3

【Automator活用術】Macで生産性を上げる作業の自動化術

今回はMacに標準でインストールされているアプリ「Automator」を使ってできる ...

Pythonを学びたいけど独学できる時間なんてない人へのすゝめ

執筆者は大学の研究室・大学院にて独学でPythonを習得した。

でも社会人になったら独学で行うには時間も体力もなくて大変だ。

時間がない社会人だからこそプロの教えを乞うのが効率的。

ここでは色んなタイプに合ったプログラミングスクールの紹介をする。

  • この記事を書いた人

メガネ

Webエンジニア駆け出し。独学のPythonで天文学系の大学院を修了。常時金欠のガジェット好きでM2 Pro MacBook Pro(30万円) x Galaxy S22 Ultra(17万円)使いの狂人。自己紹介と半生→変わって楽しいの繰り返しレビュー依頼など→お問い合わせ運営者情報、TwitterX@m_ten_pa、 YouTube@megatenpa、 Threads@megatenpa

-Python基礎
-,