2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonの整数をめぐる冒険

2
Posted at

はじめに

先日、「Pythonの文法の驚き」という記事を投稿した。そうしたら、@shiracamus(しらかみゅ)さんがコメントで「int型もオブジェクトだよ」と教えてくださった。そして__sizeof__()で、そのPythonの深淵に誘ってくださった。

Pythonのデータ型

int型もオブジェクトだということに驚きませんでしたか?
メモリが許す限り大きな数を扱えます。

>>> 0 .__sizeof__()  # 小数点に解釈されないようにドットの前に空白
28
>>> help(0 .__sizeof__)
Help on built-in function __sizeof__:

__sizeof__() method of builtins.int instance
   Returns size in memory, in bytes.
>>> 0x3fffffff .__sizeof__()
28
>>> 0x40000000 .__sizeof__()
32
>>> (1<<9999).__sizeof__()  # 1万ビット長整数
1360

このコメントをきっかけにPythonの整数オブジェクトを探検してきた。
なお、〇byte目というとき、この記事では0から数えている。

最初の調査

現代っ子である私は、GeminiにPythonのint型の実体について教えてくれと頼んだ。その結果、以下を教えてもらった。

  • 小さい整数のオブジェクトは、24byteのヘッダが4byte(=32bit)の実データについている構造体になっている
  • 30bitごとにデータ部が4byteずつ増えていく

ほう。それなら中身を見ていこう。

実験道具

メモリ上のデータを見るのには、以下のコードを使った。
環境は Python 3.14 on Kali 2026.01。

integer.py
#!/usr/bin/python3
import sys
import ctypes

def inspect_int(target, read_size):
    addr=id(target)

    header_size=24
    data=ctypes.string_at(addr, read_size)

    print(f"--- target: {hex(target)} ---")
    print(f"size:{target.__sizeof__()}")
    print(f"address: {hex(addr)}")
    print(f"header: {data[:header_size].hex(' ')}")
    print(f"value: {data[header_size:].hex(' ')}")

#######################################
inspect_int(0x0,32)
inspect_int(0x3,32)
# といった感じに実験コードを書く

実験結果

--- target: 0x0 ---
size:28
address: 0xa56c08
header: ff ff ff ff 00 00 00 00 e0 ae a1 00 00 00 00 00 01 00 00 00 00 00 00 00
value: 00 00 00 00 00 00 00 00
--- target: 0x3 ---
size:28
address: 0xa56c68
header: ff ff ff ff 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00
value: 03 00 00 00 00 00 00 00
--- target: 0x345 ---
size:28
address: 0x7f13f616fa90
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00
value: 45 03 00 00 00 00 00 00
--- target: 0x12345678 ---
size:28
address: 0x7f13f616d8f0
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00
value: 78 56 34 12 00 00 00 00
--- target: 0x3fffffff ---
size:28
address: 0x7f13f5d5ca70
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00
value: ff ff ff 3f 01 00 00 00
--- target: 0x40000000 ---
size:32
address: 0x7f13f5d5cb30
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 10 00 00 00 00 00 00 00
value: 00 00 00 00 01 00 00 00
--- target: 0xfffffffffffffff ---
size:32
address: 0x7f13f5d5cb70
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 10 00 00 00 00 00 00 00
value: ff ff ff 3f ff ff ff 3f

この出力結果を見ながら、説明していく。

ごく小さい整数

まず最初に気づいたことは、0x0と0x3のメモリアドレス(上記ではaddress:の行)が実行のたびに変わらないことだ。それ以外の数(0x345や0x3fffffff)は、メモリアドレスが実行のたびに変わる。何かおかしい。

Geminiに聞くと以下を教えてくれた。

  • CPythonの実装では、-5 から 256 まで の整数を、起動時にあらかじめメモリ上に作成して使い回している。これらは「不滅の定数(Immortal Objects)」として、メモリ上の固定位置に置かれる。
  • 0, 1, -1, 100 などの小さな整数はプログラム中で高頻度で使われる。その度に malloc(構造体の作成)と free(参照カウンタによる解放)を繰り返すのは無駄だから。
  • -5~256以外の整数は、必要になったらメモリ上にオブジェクトをつくる。

なるほど、その理由と実装は理解できる。-5、-6、256、257についても出力を確認し、-5~256が特別扱いされていることを確認した。-5から256までの整数は若いメモリアドレスの場所を、必要に応じて作られるそれ以外の整数は後ろの方の0x7f..を使っているのも納得感がある。

小さな正の整数

ごく小さい整数のメモリアドレスを見てみよう。
0x0のメモリアドレスは0xa56c08、0x3のメモリアドレスは0xa56c68となっている。整数が3つ進むと0x60メモリアドレスが増えるということで、1つの整数には0x20byte=32byte割り当てられている。
sizeofの出力を見ると28となっているので、4差がある。これはメモリアアラインメントで、メモリアドレスが8byteの倍数になるようにしているため。不滅の定数以外のアラインメントについては、後で確認する(16byteの倍数でアラインメントしていた)。

メモリアドレスやsizeofの出力はbyte単位。bit単位で数えるものはない。
例えば、メモリアドレスを表示するものとしては、objdump -D <filename>がある。objdumpの表示ではメモリアドレスとメモリの中身が逆アセンブルした結果とともに表示される。その時のメモリアドレスとメモリの中身を見比べれば、byte単位で書いてあることが実感できる。

データの詰め方

30bitごとに4byte増えるを見てみよう。
0x345のvalueがわかりやすい。45 30 00 00と書いてある。これは、Little Endianの表示である。「Little Endian」は、ゆで卵を細い方から割る人を意味し、コンピュータの世界では小さい方の桁から1byteずつ表示する方法を意味する。1byteずつというのが気持ち悪くて、2文字のセットの左側の数字が16の位で右側の数字が1の位であることは、Endianによらない。
したがって、address 0x7f...90には0x45が、0x7f...91には0x03が入っている。つまり、下の桁からbyte単位で、若いアドレスにデータを詰めていっている。これは0x12345678を見ても矛盾がない。
ここで、小さい桁の数字が若いアドレスに突っ込まれているのは、CPUアーキテクチャがx86_64で、Little Endianだからである。

一点注意。0x3fffffffのvalueがff ff ff 3f 01 00 00 00となっていることに気付いたと思う。そして01に疑問を持ったかもしれない。が、この01は無視すべきものである。なぜなら、0x3fffffffのsizeは28であり、28byte目以降は見てはいけない。28byte目の01は初期化されていないだけである。

次に0xfffffffffffffffを見てみよう。これはfが15個並んでおり、2進数で表示すると1が4*15=60個並んだ数である。これのvalueの行をみるとff ff ff 3f ff ff ff 3fとなっており、3byte目と7byte目の最上位ビット2bitは、データを入れるのに使われていない。
このデータを入れない2bitが、4byte=32bitなのに、30bitごとに4byte必要になる理由である。

Geminiに聞くと以下を教えてくれた。

  • この2bitは掛け算のときの繰り上がりに利用する。
  • 昔、32bitアーキテクチャの頃は、4byte(32bitの変数)の中に15bitずつ詰めていた。

大きい整数

もっと大きい整数も見てみよう

output
--- target: 0x1234567812345678ffffffff ---
size:40
address: 0x7f13f5db9bf0
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 20 00 00 00 00 00 00 00
value: ff ff ff 3f e3 59 d1 08 81 67 45 23 04 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00
--- target: 0x123456789abcdef12345678ffffffff ---
size:44
address: 0x7f13f5db9cb0
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 28 00 00 00 00 00 00 00
value: ff ff ff 3f e3 59 d1 08 f1 de bc 1a e2 59 d1 08 01 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00

この出力から、前の章で説明したとおりに、30bitずつ4byte確保して、下の桁から確保していっていることがわかる。
ここで注目すべきは、4byteのブロックで見ても、若いアドレスに小さい桁の数字が突っ込まれており、ブロックの内側も外側もLittle Endianになっていることである。

ヘッダ

ヘッダ24byteも見ていこう。題材は0x345とするが、ほかの整数も見ると納得が深まる。

オフセット 項目名 値 説明
0 ob_refcnt 03 00 00 00 00 00 00 00 参照カウンタ。使われているから0でない。ガベージコレクションするかしないかを判断できる
+8 ob_type e0 ae a1 00 00 00 00 00 型ポインタ。int 型であることを示す構造体へのメモリアドレス。これがあるから type(obj) が判定できる。別途実験した結果、str型では、a0 ab a1になっていた。
+16 lv_tag 08 00 00 00 00 00 00 00 サイズ+状態フラグ。説明が長くなったので、下の本文に書く。

lv_tagの説明。
一番若いアドレスの、下から3bitは正負や状態を表す。
4byteをいくつ使っているかは、3ビット左シフトして表す。0x345は正の数なので、最下位2bitは0。4byteを1つ使っているので、0000 0001を3ビット左シフトして0000 1000。最下位2ビットと合わせて0x8となっている。
4byteを2つ3つ使うような数は、08の代わりに10や18になっている。
負の整数については次の節で見る。

lv_tagはPython 3.12で変わった。それまではob_sizeと呼ばれていて、表現方法も今とは違っていたとのこと。

負の整数

output
--- target: 0x0 ---
size:28
address: 0xa56c08
header: ff ff ff ff 00 00 00 00 e0 ae a1 00 00 00 00 00 01 00 00 00 00 00 00 00
value: 00 00 00 00 00 00 00 00
--- target: 0x3 ---
size:28
address: 0xa56c68
header: ff ff ff ff 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00
value: 03 00 00 00 00 00 00 00
--- target: -0x87 ---
size:28
address: 0x7f13f5d5cb10
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 0a 00 00 00 00 00 00 00
value: 87 00 00 00 01 00 00 00
--- target: 0xfffffffffffffffffffffffffffff ---
size:40
address: 0x7f13f5db9d70
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 20 00 00 00 00 00 00 00
value: ff ff ff 3f ff ff ff 3f ff ff ff 3f ff ff ff 03 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00
--- target: -0xfffffffffffffffffffffffffffff ---
size:40
address: 0x7f13f5dba9d0
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 22 00 00 00 00 00 00 00
value: ff ff ff 3f ff ff ff 3f ff ff ff 3f ff ff ff 03 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 40 ad a1 00 00 00 00 00

lv_tagの最下位3bitは、状態を表す。0x01は0であることを表す。0x02は負の数をあることを表す。という説明と上の結果は整合している。

Cでは負の数は2の補数で表す。しかし、Pythonでは可変長のBignumを使うので、上の方の桁を1で(ffで)埋める表現は計算上便利ではない。そのため、正負をlv_tagで、絶対値をdataとしてもっている。

整数オブジェクトのメモリアラインメント

メモリアラインメントを確かめるために、次のコードを走らせてみた。

integer3.py
#!/usr/bin/python3
import sys
import ctypes
import math

def inspect_int(target, read_size):
    addr=id(target)

    header_size=24
    bit_len = target.bit_length()


    data=ctypes.string_at(addr, read_size)

    print(f"--- target: {hex(target)} ---")
    print(f"target bit length(** in bits **):{bit_len}")
    print(f"size:{target.__sizeof__()}")
    print(f"address: {hex(addr)}")
    print(f"header: {data[:header_size].hex(' ')}")
    print(f"value: {data[header_size:].hex(' ')}")

inspect_int(0x345,64)
inspect_int(0x12345678,64)
inspect_int(0x3fffffff,64)
inspect_int(0x40000000,64)
inspect_int(0x56781234,64)
inspect_int(0xffffffff,64)
inspect_int(0x12345678ffffffff,64)
inspect_int(0x98765432ffffffff,64)
inspect_int(0x98765432ffffffff98765432,64)
inspect_int(0xF23456789abcdef123456789abcdef,64)
inspect_int(0xF23456789abcdef123456789abcdef123456789abcdef,64)
inspect_int(0xF23456789abcdef123456789abcdef123456789abcdef123456789abcdef,128)
output
--- target: 0x345 ---
target bit length(** in bits **):10
size:28
address: 0x7fc5c05198f0
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00
value: 45 03 00 00 22 29 0a 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00 f0 0c 0d 03 00 00 00 00
--- target: 0x12345678 ---
target bit length(** in bits **):29
size:28
address: 0x7fc5c051ba90
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00
value: 78 56 34 12 00 00 00 00 01 00 00 00 00 00 00 00 c0 2b a1 00 00 00 00 00 ea ed e0 f9 3a 6f da 41 00 00 00 00 00 00 00 00
--- target: 0x3fffffff ---
target bit length(** in bits **):30
size:28
address: 0x7fc5c0458a70
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00
value: ff ff ff 3f 01 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 08 00 00 00 00 00 00 00 60 9d a9 37 00 00 00 00
--- target: 0x40000000 ---
target bit length(** in bits **):31
size:32
address: 0x7fc5c0458b30
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 10 00 00 00 00 00 00 00
value: 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00
--- target: 0x56781234 ---
target bit length(** in bits **):31
size:32
address: 0x7fc5c0458b70
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 10 00 00 00 00 00 00 00
value: 34 12 78 16 01 00 00 00 02 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 10 00 00 00 00 00 00 00 ff ff ff 3f 03 00 00 00
--- target: 0xffffffff ---
target bit length(** in bits **):32
size:32
address: 0x7fc5c0458b90
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 10 00 00 00 00 00 00 00
value: ff ff ff 3f 03 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 10 00 00 00 00 00 00 00 34 12 78 16 01 00 00 00
--- target: 0x12345678ffffffff ---
target bit length(** in bits **):61
size:36
address: 0x7fc5c04b5b90
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 18 00 00 00 00 00 00 00
value: ff ff ff 3f e3 59 d1 08 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00
--- target: 0x98765432ffffffff ---
target bit length(** in bits **):64
size:36
address: 0x7fc5c04b5c50
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 18 00 00 00 00 00 00 00
value: ff ff ff 3f cb 50 d9 21 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00
--- target: 0x98765432ffffffff98765432 ---
target bit length(** in bits **):96
size:40
address: 0x7fc5c04b5d10
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 20 00 00 00 00 00 00 00
value: 32 54 76 18 fe ff ff 3f 2f 43 65 07 26 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00
--- target: 0xf23456789abcdef123456789abcdef ---
target bit length(** in bits **):120
size:40
address: 0x7fc5c04b5dd0
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 20 00 00 00 00 00 00 00
value: ef cd ab 09 9e 15 8d 04 ef cd ab 09 9e 15 8d 3c 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00
--- target: 0xf23456789abcdef123456789abcdef123456789abcdef ---
target bit length(** in bits **):180
size:48
address: 0x7fc5c04b5e90
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 30 00 00 00 00 00 00 00
value: ef cd ab 09 9e 15 8d 04 ef cd ab 09 9e 15 8d 04 ef cd ab 09 9e 15 8d 3c 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00
--- target: 0xf23456789abcdef123456789abcdef123456789abcdef123456789abcdef ---
target bit length(** in bits **):240
size:56
address: 0x7fc5c04c52b0
header: 03 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 40 00 00 00 00 00 00 00
value: ef cd ab 09 9e 15 8d 04 ef cd ab 09 9e 15 8d 04 ef cd ab 09 9e 15 8d 04 ef cd ab 09 9e 15 8d 3c 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 e0 ae a1 00 00 00 00 00 40 00 00 00 00 00 00 00 ef cd ab 09 9e 15 8d 04 ef cd ab 09 9e 15 8d 04 ef cd ab 09 9e 15 8d 04 ef cd ab 09 9e 15 8d 3c 00 00 00 00 00 00 00 00

まず、着目すべきことは、メモリアドレスの末尾がすべて0になっていること。これは、(8byteではなく)16byteでメモリアラインしていることを示唆している。
このテストコードでは64byte分データとして取ってきている。一方、例えば0x345のsizeの行に28と表示されていることからわかるように、0x345のデータはヘッダを含めて28byte分で、valueの後ろの方(正確には4byte目以降)のデータは0x345オブジェクトのデータではない。そこに、次のデータが見えていないか見てみよう。
valueの後ろの方を見ると連続する0の間にe0 ae a1が見つかる。これは、後ほど見るように、オブジェクトヘッダのxxxを示唆している。とすると、そこからxxbyteさかのぼったvalueの8byte目(01)から次のデータオブジェクトが始まっていそうである。つまり、0345のメモリアドレス+32byteの位置に次のデータオブジェクトがありそうである。
次に見るのは、sizeが32を超えるところである。ここでは、0x98765432ffffffffが36byteのデータとなる。これを前の行のsize32の整数のデータと比べると、e0 ae a1の位置が16byte分後ろにずれている。この位置はsizeが48までは同じであり、sizeが56になるとさらに16ビット分後ろにずれる。
ということは、ユーザ作成の整数オブジェクトは16byteでアラインしてそうである。

sizeof()

そうするとメモリの物理的な専有サイズは、sizeof()で返ってくる数字ではなく、sizeof()で返ってくる数字を16で切り上げた数である。sizeof()で返ってくる数字は、純粋にオブジェクト本体が消費している内部サイズである。
一方、C言語のsizeof()演算子はメモリの専有サイズ(アライン後)のサイズを返すので、0x345みたいな整数構造体を作ると、sizeof()は28ではなく32を返す。

結びに

Pythonは高級言語なので、整数の内部表現は気にしなくても使える。高級言語としてそうあるべきである。が、どのような仕組みで動いているかを知ることは喜びである。仕組みを知ることで、言語設計者の思想(目指すところ)を覗くことができる。Pythonの整数をめぐる大冒険だった。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?