🐥

CPython 3.13 のバイトコードを実験で読み解く

に公開

はじめに

本ドキュメントはPythonのバイトコードについて調査と実験したものをまとめます。

バイトコードはPython プログラムの CPython インタプリタの内部表現です。

バイトコードの知見を得ることで、パフォーマンス上の問題の解析や、書き方の良し悪しを根拠をもって説明できます。たとえば、単なる条件分岐で済む箇所を例外で制御しているコードをレビューする際、感覚的に「直して」ではなく、次のように言えます

「Python の例外処理は“例外が発生しない限り”はゼロコスト設計ですが、例外が発生すると ExceptionTable の参照→スタック深さの調整→(必要なら)lasti を積む→ハンドラへ分岐→RERAISE 等の経路を辿るため、通常フローより高コストです。ここはロジック分岐にすると処理が軽く、読みやすさも上がります。」

また、実際、バイトコードを活用したプログラムには以下のようなものがあります。

  • pytype
    • 最終対応が 3.12の歴史のある型チェッカー
  • python-goto
    • PythonでGOTO構文を実現するライブラリ

なお、本ドキュメントは後述の条件で実験して、記載したものです。
環境が異なれば動作しませんし、記載内容について実験ミス、ドキュメントの誤読による誤った結論が記載されていることがあります。

簡単なバイトコードの生成と確認

Pythonのスクリプトが、どのようにバイトコードを作って動作するかの大まかな流れは以下のようになります。

  • Parser が構文解析を行い、AST(抽象構文木)を生成します。
  • その後 Compiler が AST から実行順序を示す論理構造である 制御フローグラフ(CFG) を作成します。
  • Assembler が CFG を、逐次実行可能な命令列である バイトコードに変換します。

CPython ではコンパイルの起点は モジュール(ファイル) です。関数・クラス・内包表記などのブロックは、それぞれ独立した実行単位としてまとめられます。
設定や実行方法によっては、コンパイル済みバイトコードは .pyc としてキャッシュされる場合があります。次回、同じモジュールを再インポートする際は再コンパイルが省略されます。
重要な注意として、バイトコードは他のPython実装間での互換性はなく、CPython のリリース間でも安定性は保証されません。

コマンドラインからバイトコードを確認する方法

ここでは簡単にバイトコードの内容を閲覧する方法について説明します。
以下のようなファイルが存在するとします。

/lib/demo_mod.py

def add(x, y):
    return x + y

このモジュールのバイトコードを確認するには以下のdisコマンドを実行します。

python -m dis lib/demo_mod.py
  0           RESUME                   0

  1           LOAD_CONST               0 (<code object add at 0x10de36170, file "lib/demo_mod.py", line 1>)
              MAKE_FUNCTION
              STORE_NAME               0 (add)
              RETURN_CONST             1 (None)

Disassembly of <code object add at 0x10de36170, file "lib/demo_mod.py", line 1>:
  1           RESUME                   0

  2           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP                0 (+)
              RETURN_VALUE

本来、バイトコードは命令の順序付き列です。CPython(Wordcode)では、**各命令は原則 2 バイト(1 バイトのオペコード+1 バイトのオペランド)**で表されます。ただし場合により追加の語が付くため、2 バイト固定ではありません。
上の出力は、そのバイトコードを人間向けに整形した表示です。

オペコード(opcode)は「どの命令を実行するか」を示す1バイトの識別子です。
以下にその一覧があります。
https://github.com/python/cpython/blob/3.13/Include/opcode_ids.h

それぞれのオペコードの説明については以下のページで確認できます。
https://docs.python.org/ja/3.13/library/dis.html#python-bytecode-instructions

オペランド(operand / oparg)はその命令に渡す1バイトの数値引数で、意味は命令ごとに異なります。

上記の例では人間向けに整形した表示では3列存在します。

1列目はバイトコードに対応するソースコードの行数です。
2列目はオペコードになっており、これを人間に見やすい文字として表示しています。
オペコードの一覧は以下に存在します。
3列目はオペランドの値と、それをどう解釈したかが書かれています。

今回の出力結果はモジュール本体と add 関数本体の 2 つに分かれています。

モジュール本体
RESUME 0
この命令は、内部トレース、デバッグ、および最適化チェックを実行します。
オペランドの0は「ジェネレータ、コルーチン、非同期ジェネレータではない関数の開始」を表しています。
フレームの開始を表します

LOAD_CONST 0
co_constsと呼ばれる定数の一覧からオペランドが指すインデックスの値を取得します。
今回のケースではaddの実行単位であるコードオブジェクトになります。これをスタックに積みます。

MAKE_FUNCTION
直前のコードオブジェクトから関数オブジェクトを作ります。
これにより、コードオブジェクトに実行環境を結び付けて呼び出せるようになります。

STORE_NAME 0
スタックのトップの値を名前表 co_names[0] に対応する名前に紐づけます。
今回の場合は直前に作成した関数オブジェクトをグローバル名 add に紐づけます。

RETURN_CONST 1
co_consts[1](今回の場合はNone)を呼び出し元に返します。
モジュールの評価結果として None を即座に返す(.py を実行/インポートしたときの戻り値)

add関数本体
RESUME 0
フレームの開始を表します

LOAD_FAST_LOAD_FAST 1
ローカル変数の一覧であるco_varnamesから2つの参照を取得してスタックに積みます。
オペランドの1について上位4bitと下位4bitに分割して、それをco_varnamesのインデックスとします。
今回の場合, 1>>4でco_varnames[0](今回はx)と1 & 0xfでco_varnames[1](今回はy)をスタックに積みます。

BINARY_OP 0(+)
スタック中の2つの値をポップしてオペランドで与えた演算を行い、その結果をスタックします。
Python3.13ではオペランドの種類は以下の通りです。

オペランド 名称 演算子
0 NB_ADD +
1 NB_AND &
2 NB_FLOOR_DIVIDE //
3 NB_LSHIFT <<
4 NB_MATRIX_MULTIPLY @
5 NB_MULTIPLY *
6 NB_REMAINDER %
7 NB_OR |
8 NB_POWER **
9 NB_RSHIFT >>
10 NB_SUBTRACT -
11 NB_TRUE_DIVIDE /
12 NB_XOR ^
13 NB_INPLACE_ADD +=
14 NB_INPLACE_AND &=
15 NB_INPLACE_FLOOR_DIVIDE //=
16 NB_INPLACE_LSHIFT <<=
17 NB_INPLACE_MATRIX_MULTIPLY @=
18 NB_INPLACE_MULTIPLY *=
19 NB_INPLACE_REMAINDER %=
20 NB_INPLACE_OR |=
21 NB_INPLACE_POWER **=
22 NB_INPLACE_RSHIFT >>=
23 NB_INPLACE_SUBTRACT -=
24 NB_INPLACE_TRUE_DIVIDE /=
25 NB_INPLACE_XOR ^=

上記の内容はopcodeモジュールの_nb_opsで確認できます。

BINARY_OPのオペランドの種類を確認する例

for i, (enum_name, symbol) in enumerate(_nb_ops):
     print(f"{i:2d}: {enum_name:>24s}  {symbol}")

RETURN_VALUE
呼び出し元の関数に直近にスタックされた値を返します。

Pythonのコード上でバイトコードを確認する方法

モジュールのバイトコードを確認する方法

compile組み込み関数でPythonのソースコードをコンパイルしてコードオブジェクトが取得できます。
このコードオブジェクトからdisモジュールdis関数でバイトコードの解析結果の出力が可能です。

import dis, pathlib

path = pathlib.Path("lib/demo_mod.py")
src  = path.read_text(encoding="utf-8")

code = compile(src, str(path), "exec")  # ← モジュール本体のコードオブジェクト
print(type(code)) # <class 'code'>
print(code.co_code) # 生のバイトコード
dis.dis(code)

もし、随時コンパイルするのでなく.pycのキャッシュを利用したい場合はspec_from_file_locationを使用したModuleSpecからバイトコードを確認する方法もあります。

import importlib.util, dis
spec = importlib.util.spec_from_file_location("m", "lib/demo_mod.py")
code = spec.loader.get_code("m")      # ← モジュール本体の code object
print(type(spec)) # <class '_frozen_importlib.ModuleSpec'>
print(type(code)) # <class 'code'>
print(code.co_code) # 生のバイトコード
dis.dis(code)

関数やクラスのバイトコードを確認する方法

クラスや関数からdis関数を使用してバイトコードを取得することも可能です。

単純な関数の例

disに関数を指定するだけで関数本体のコードオブジェクトからバイトコードを取得します。

関数の例

import dis
from lib.demo_mod import add

# 生のバイトコード
print(add.__code__.co_code)

# 関数addのバイトコード
dis.dis(add)

出力結果

b'\x95\x00X\x01-\x00\x00\x00$\x00'
  1           RESUME                   0

  2           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP                0 (+)
              RETURN_VALUE

クラスの例

dis関数にクラスを指定することでクラス中のメソッドのバイトコードの解析結果が表示されます。

クラスの例

import dis
import types

class X:
    def __init__(self):
        self.x = 1
    def inc(self):
        self.x += 1

# 各メソッドの生のバイトコードを取得する
_have_code = (types.MethodType, types.FunctionType, types.CodeType,
              classmethod, staticmethod, type)
items = sorted(X.__dict__.items())
for name, x1 in items:
    if isinstance(x1, _have_code):
        print(name, x1.__code__.co_code)

# バイトコードを人間が見れるようにする
dis.dis(X)

出力結果

__init__ b'\x95\x00S\x01U\x00l\x00\x00\x00\x00\x00\x00\x00\x00\x00g\x00'
inc b'\x95\x00U\x00=\x01R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x01-\r\x00\x00s\x02l\x00\x00\x00\x00\x00\x00\x00\x00\x00g\x00'
Disassembly of __init__:
  5           RESUME                   0

  6           LOAD_CONST               1 (1)
              LOAD_FAST                0 (self)
              STORE_ATTR               0 (x)
              RETURN_CONST             0 (None)

Disassembly of inc:
  7           RESUME                   0

  8           LOAD_FAST                0 (self)
              COPY                     1
              LOAD_ATTR                0 (x)
              LOAD_CONST               1 (1)
              BINARY_OP               13 (+=)
              SWAP                     2
              STORE_ATTR               0 (x)
              RETURN_CONST             0 (None)

バイトコードの解析情報の詳細を使用する方法

dis関数はバイトコードを解析してオペコードやオペランドを出力するには便利です。
しかし、オペコードやオペランドをプログラム中に使用したいケースには使用できません。
解析結果のオペコードやオペランドをプログラム中で使用したい場合はdisモジュールのBytecodeクラスを使用します。
Bytecodeクラスに関数を与えた場合、Instructionのシーケンスが取得できるので、それを活用できます。

サンプル

from lib.demo_mod import add
import dis

for ins in dis.Bytecode(add):
    print(f"offset: {ins.offset}")
    print(f"  opcode: {ins.opname}({ins.opcode})")
    print(f"  baseopcode: {ins.baseopname}({ins.baseopcode})")
    print(f"  arg: {ins.argval}({ins.arg})")

実はバイトコードは実行中に最適化のために書き変わるケースがあります。
オリジナルのオペコードが必要な場合はbaseopcode, baseopnameを使用して、適応後のオペコードが必要な場合はopnameとopcodeを使用します。
詳細については後述します。

生のバイトコードを解析する方法

コードオブジェクトのco_codeから生のバイトコードが取得できます。
ここでは、その生データを直接解析する方法を説明します。

サンプル

import struct
import dis

def dump_code(f):
    code = f.__code__.co_code
    opcode, oparg = struct.unpack_from('BB', code, 2)
    pos = 0
    extended_arg = 0
    extended_arg_offset = None
    while pos < len(code):
        offset = pos
        if extended_arg_offset is not None:
            offset = extended_arg_offset
        opcode = struct.unpack_from('B', code, pos)[0]
        pos += 1
        if opcode in dis.hasarg:
            # 引数あり
            oparg = extended_arg | struct.unpack_from('B', code, pos)[0]
            pos += 1
            if opcode == dis.EXTENDED_ARG:
                extended_arg = oparg << 8
                extended_arg_offset = offset
                continue
        else:
            # 引数がないケース
            pos += 1
            oparg = None
        extended_arg = 0
        extended_arg_offset = None
        print(f"  offset:{offset} opcode: {dis.opname[opcode]}({opcode}) args: {oparg}")


EXTENDED_ARGという1バイトに収まらないオペランドをもつオペコードに対応するための命令のため、ややこしい実装になっていますが、基本的にはオペコードとオペランドの組み合わせを取得するだけになります。

add関数に対してdump_codeを実行した結果は以下の通りとなります。

>>from lib.demo_mod import add
>>dump_code(add)
  offset:0 opcode: RESUME(149) args: 0
  offset:2 opcode: LOAD_FAST_LOAD_FAST(88) args: 1
  offset:4 opcode: BINARY_OP(45) args: 0
  offset:6 opcode: CACHE(0) args: 0
  offset:7 opcode: CACHE(0) args: 0
  offset:8 opcode: RETURN_VALUE(36) args: None

dis関数の結果と違い、CACHEという命令が増えています。
これは実行中に最適化のために使用されるキャッシュの情報です。詳細については後述します。

適応的スペシャライザ

バイトコードは実行中に最適化されて書き変わるケースがあります。
詳細については以下に記載があります。

PEP 659 – Specializing Adaptive Interpreter

たとえば以下の関数があります。

def add(x, y):
    return x + y

この関数のバイトコードが以下のようになります。

  5           RESUME                   0

  6           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP                0 (+)
              RETURN_VALUE

add関数は引数が文字型のときも、数値型の場合も動作します。
しかし、多くの場合、この関数は同じ型で実行されるケースが多いはずです。

つまり整数が頻繁に渡される場合はBINARY_OPの代わりにBINARY_OP_ADD_INTを変更した方がパフォーマンスは良くなります。

  5           RESUME                   0

  6           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP_ADD_INT        0 (+)
              RETURN_VALUE

仮に文字列が渡される場合はBINARY_OPの代わりにBINARY_OP_ADD_UNICODEに変更します。

  5           RESUME                   0

  6           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP_ADD_UNICODE    0 (+)
              RETURN_VALUE

特殊化適応的インタープリタでは、このように実行状況にあわせて、バイトコードを変更します。

動作例

実際に、どのように特殊化が適応されるか観察してみます。
特殊化されたバイトコードを解析する場合は、dis関数のadaptiveとshow_cachesをTrueとして実行します。

dis.dis(add, adaptive=True, show_caches=True)

以下が実際に特殊化が適応される例になります。

サンプルコード

import dis

def add(x, y):
    return x + y

print("未実行----------------")
dis.dis(add, adaptive=True, show_caches=True)

print("数値型 1回実行----------------")
add(1, 1)
dis.dis(add, adaptive=True, show_caches=True)

print("数値型 2回実行----------------")
add(1, 2)
dis.dis(add, adaptive=True, show_caches=True)

print("数値型 3回実行----------------")
add(1, 3)
dis.dis(add, adaptive=True, show_caches=True)

print("文字型 1回実行----------------")
add("1", "1")
dis.dis(add, adaptive=True, show_caches=True)

print("文字型 大量に実行----------------")
for i in range(200): add("1", "x")
dis.dis(add, adaptive=True, show_caches=True)

未実行の場合にバイトコードを見た場合、以下のようになります。

  3           RESUME                   0

  4           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP                0 (+)
              CACHE                    0 (counter: 17)
              RETURN_VALUE

この状態ではBINARY_OPのままです。
直後にCACHEがありますが、ここにBINARY_OPの使用状況を確認して特殊化が行われます。
CACHEの内容はオペコードごとに異なり、BINARY_OPの場合は16 ビットのバックオフ付きカウンタ 1 語のみとなります。

16ビットのカウンタは、12ビットの符号なし “value” と 4ビットの “backoff” フィールドから構成されます。
今回の場合はcounterが17なので以下のvalueは1、backoffは1となります。

参考

>>> n = 17
>>> n >> 4
1
>>> n & 0xF
1

これは、「あと1回“汎用実行”してプロファイルを溜めたら、次回に特殊化を試みる」 という段階です。

では、この状態で整数を引数にaddを実行したのちのバイトコードの解析結果は以下のようになります。

数値型 1回実行----------------
  3           RESUME_CHECK             0

  4           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP                0 (+)
              CACHE                    0 (counter: 1)
              RETURN_VALUE

BINARY_OPのままで特殊化は行われておらず、counterは1であるため、valueは0、backoffは1となります。
特殊化は 次にその命令に到達したとき に試みられます。

2回目の整数引数のaddを実行したのちのバイトコードの解析結果は以下のようになります。

数値型 2回実行----------------
  3           RESUME_CHECK             0

  4           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP_ADD_INT        0 (+)
              CACHE                    0 (counter: 832)
              RETURN_VALUE

BINARY_OPがBINARY_OP_ADD_INTと特殊化されました。
counter: 832 は value=52, backoff=0 を意味します。特殊化後はガード失敗(型が合わない実行)が起こるたびに value が減り、0 になると再特殊化やデオプトを試みます。ガード成功(想定型)では減りません。

すでに整数で特殊化されている状態で3回目の整数引数のaddを呼ぶと以下のようになります。

数値型 3回実行----------------
  3           RESUME_CHECK             0

  4           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP_ADD_INT        0 (+)
              CACHE                    0 (counter: 832)
              RETURN_VALUE

このケースではガードが成功したためcounterの数値は変化しません。
この状態で文字型の引数でaddを呼ぶと以下のようになります。

文字型 1回実行----------------
  3           RESUME_CHECK             0

  4           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP_ADD_INT        0 (+)
              CACHE                    0 (counter: 816)
              RETURN_VALUE

BINARY_OP_ADD_INTのままですが、ガードが失敗したためcounterのvalueが52->51に減っています。

この状況でさらに大量に文字型の引数でaddを呼ぶと以下のようになります。

文字型 大量に実行----------------
  3           RESUME_CHECK             0

  4           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP_ADD_UNICODE    0 (+)
              CACHE                    0 (counter: 832)
              RETURN_VALUE

BINARY_OP_ADD_INT->BINARY_OP_ADD_UNICODEに変更されていることが確認できます。

特殊化のタイミングで別の型を指定した場合

数値型一回目で特殊化のタイミングが来たのち、文字型を指定した場合の結果は以下のとおりです。

サンプルコード

import dis

def add(x, y):
    return x + y

print("未実行----------------")
dis.dis(add, adaptive=True, show_caches=True)

print("数値型 1回実行----------------")
add(1, 1)
dis.dis(add, adaptive=True, show_caches=True)

print("文字型 1回実行----------------")
add("1", "1")
dis.dis(add, adaptive=True, show_caches=True)

結果

文字型 1回実行----------------
  3           RESUME_CHECK             0

  4           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP_ADD_UNICODE    0 (+)
              CACHE                    0 (counter: 832)
              RETURN_VALUE

BINARY_OP_ADD_UNICODEになっていることが確認できます。
つまり特殊化のタイミングのときに、その時の型で最適のものが選ばれていることがわかります。

特殊化の対象のオペコード

dis._specializationsで汎用命令と、その特殊命令の対応が取得できます。
特殊命令のオペコードについてはdis._specialized_opmapで取得できます。
また、使用するキャッシュの型も汎用命令の種類によって構造体が変わります。

Python3.13の場合は以下のようになります。

汎用命令 特殊型 キャッシュの型
RESUME RESUME_CHECK(207) キャッシュは付与されない
TO_BOOL TO_BOOL_ALWAYS_TRUE(214)
TO_BOOL_BOOL(215)
TO_BOOL_INT(216)
TO_BOOL_LIST(217)
TO_BOOL_NONE(218)
TO_BOOL_STR(219)
_PyToBoolCache
BINARY_OP BINARY_OP_MULTIPLY_INT(154)
BINARY_OP_ADD_INT(151)
BINARY_OP_SUBTRACT_INT(156)
BINARY_OP_MULTIPLY_FLOAT(153)
BINARY_OP_ADD_FLOAT(150)
BINARY_OP_SUBTRACT_FLOAT(155)
BINARY_OP_ADD_UNICODE(152)
BINARY_OP_INPLACE_ADD_UNICODE(3)
_PyBinaryOpCache
BINARY_SUBSCR BINARY_SUBSCR_DICT(157)
BINARY_SUBSCR_GETITEM(158)
BINARY_SUBSCR_LIST_INT(159)
BINARY_SUBSCR_STR_INT(160)
BINARY_SUBSCR_TUPLE_INT(161)
STORE_SUBSCR STORE_SUBSCR_DICT(212)
STORE_SUBSCR_LIST_INT(213)
_PyStoreSubscrCache
SEND SEND_GEN(208) _PySendCache
UNPACK_SEQUENCE UNPACK_SEQUENCE_TWO_TUPLE(222)
UNPACK_SEQUENCE_TUPLE(221)
UNPACK_SEQUENCE_LIST(220)
_PyUnpackSequenceCache
STORE_ATTR STORE_ATTR_INSTANCE_VALUE(209)
STORE_ATTR_SLOT(210)
STORE_ATTR_WITH_HINT(211)
_PyAttrCache
LOAD_GLOBAL LOAD_GLOBAL_MODULE(204)
LOAD_GLOBAL_BUILTIN(203)
_PyLoadGlobalCache
LOAD_SUPER_ATTR LOAD_SUPER_ATTR_ATTR(205)
LOAD_SUPER_ATTR_METHOD(206)
_PySuperAttrCache
LOAD_ATTR LOAD_ATTR_INSTANCE_VALUE(193)
LOAD_ATTR_MODULE(197)
LOAD_ATTR_WITH_HINT(202)
LOAD_ATTR_SLOT(201)
LOAD_ATTR_CLASS(191)
LOAD_ATTR_PROPERTY(200)
LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN(192)
LOAD_ATTR_METHOD_WITH_VALUES(196)
LOAD_ATTR_METHOD_NO_DICT(195)
LOAD_ATTR_METHOD_LAZY_DICT(194)
LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES(199)
LOAD_ATTR_NONDESCRIPTOR_NO_DICT(198)
_PyLoadMethodCache
COMPARE_OP COMPARE_OP_FLOAT(182)
COMPARE_OP_INT(183)
COMPARE_OP_STR(184)
_PyCompareOpCache
CONTAINS_OP CONTAINS_OP_SET(186)
CONTAINS_OP_DICT(185)
_PyContainsOpCache
FOR_ITER FOR_ITER_LIST(188)
FOR_ITER_TUPLE(190)
FOR_ITER_RANGE(189)
FOR_ITER_GEN(187)
_PyForIterCache
CALL CALL_BOUND_METHOD_EXACT_ARGS(163)
CALL_PY_EXACT_ARGS(177)
CALL_TYPE_1(181)
CALL_STR_1(179)
CALL_TUPLE_1(180)
CALL_BUILTIN_CLASS(165)
CALL_BUILTIN_O(168)
CALL_BUILTIN_FAST(166)
CALL_BUILTIN_FAST_WITH_KEYWORDS(167)
CALL_LEN(170)
CALL_ISINSTANCE(169)
CALL_LIST_APPEND(171)
CALL_METHOD_DESCRIPTOR_O(175)
CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS(173)
CALL_METHOD_DESCRIPTOR_NOARGS(174)
CALL_METHOD_DESCRIPTOR_FAST(172)
CALL_ALLOC_AND_ENTER_INIT(162)
CALL_PY_GENERAL(178)
CALL_BOUND_METHOD_GENERAL(164)
CALL_NON_PY_GENERAL(176)
_PyCallCache

特殊化されたバイトコードの解析情報の詳細を使用する方法

disモジュールのByteCodeクラスを使用して特殊化されたコードを取得するサンプルは以下のとおりです。
Bytecodeのadaptive=True, show_caches=Trueをすることで、cache_infoが取得可能になります。

サンプル

import dis

def add(x, y):
    return x + y

# 特殊化するために複数回呼び出す
for i in range(200): add(1, 2)

for ins in dis.Bytecode(add, adaptive=True, show_caches=True):
    print(f"offset: {ins.offset}")
    print(f"  opcode: {ins.opname}({ins.opcode})")
    print(f"  baseopcode: {ins.baseopname}({ins.baseopcode})")
    print(f"  arg: {ins.argval}({ins.arg})")
    if ins.cache_info:  # ここがインラインキャッシュ(構造化済み)
        print("     cache_info:", ins.cache_info)
        print("     cache range:", ins.cache_offset, "→", ins.end_offset)

出力結果

offset: 0
  opcode: RESUME_CHECK(207)
  baseopcode: RESUME(149)
  arg: 0(0)
offset: 2
  opcode: LOAD_FAST_LOAD_FAST(88)
  baseopcode: LOAD_FAST_LOAD_FAST(88)
  arg: ('x', 'y')(1)
offset: 4
  opcode: BINARY_OP_ADD_INT(151)
  baseopcode: BINARY_OP(45)
  arg: 0(0)
     cache_info: [('counter', 1, b'@\x03')]
     cache range: 6 → 6
offset: 8
  opcode: RETURN_VALUE(36)
  baseopcode: RETURN_VALUE(36)
  arg: None(None)

特殊化された生のバイトコードを解析する方法

特殊化される前のバイトコードはコードオブジェクト.__code__.co_codeに格納されていますが、特殊化されたものは非公開のコードオブジェクト.__code__._co_code_adaptiveに格納されています。

前述したdump_code()についてはオペコードがCACHE時の挙動について変更する必要もあります。

サンプルコード

import struct
import dis

# 特殊化のオペコードとその名前を構築
dict_specialized_codes = {v: k for k, v in dis._specialized_opmap.items()}

# 特殊化された命令はdis.opnameにないので自分で対応表をつくる
def get_op_name(opcode):
    name = dict_specialized_codes.get(opcode)
    if not name:
        name = dis.opname[opcode]
    return name

def dump_adaptive_code(f):
    # 特殊化の場合は_co_code_adaptiveにバイトコードがある
    code = f.__code__._co_code_adaptive
    original_code = f.__code__.co_code
    opcode, oparg = struct.unpack_from('BB', code, 2)
    pos = 0
    extended_arg = 0
    extended_arg_offset = None
    while pos < len(code):
        offset = pos
        if extended_arg_offset is not None:
            offset = extended_arg_offset
        opcode = struct.unpack_from('B', code, pos)[0]
        pos += 1
        original_op = original_code[offset]
        if original_op == 0:
            # オリジナルのOpCodeがCACHEDの場合、キャッシュの内容をそのままとる
            args = struct.unpack_from('B', code, pos)[0]
            print("          cache:", hex(opcode), hex(args), (opcode+(args<<8)))
            pos += 1
            continue
        if original_op in dis.hasarg:
            # 引数あり
            oparg = extended_arg | struct.unpack_from('B', code, pos)[0]
            pos += 1
            if opcode == dis.EXTENDED_ARG:
                extended_arg = oparg << 8
                extended_arg_offset = offset
                continue
        else:
            # 引数がないケース
            pos += 1
            oparg = None
        extended_arg = 0
        extended_arg_offset = None
        print(f"  offset:{offset} opecode: {get_op_name(opcode)}({opcode}) args: {oparg}")

def add(x, y):
    return x + y
print('適応前')
dump_adaptive_code(add)

# 特殊化するために複数回呼び出す
for i in range(200): add(1, 2)
print('適応後')
dump_adaptive_code(add)

出力結果

適応前
  offset:0 opcode: RESUME(149) args: 0
  offset:2 opcode: LOAD_FAST_LOAD_FAST(88) args: 1
  offset:4 opcode: BINARY_OP(45) args: 0
          cache: 0x11 0x0 17
  offset:8 opcode: RETURN_VALUE(36) args: None
適応後
  offset:0 opcode: RESUME_CHECK(207) args: 0
  offset:2 opcode: LOAD_FAST_LOAD_FAST(88) args: 1
  offset:4 opcode: BINARY_OP_ADD_INT(151) args: 0
          cache: 0x40 0x3 832
  offset:8 opcode: RETURN_VALUE(36) args: None

バイトコードと.pycファイル

PYTHONDONTWRITEBYTECODE環境変数が未設定や0の場合、モジュールをインポートした際にモジュールのコンパイルが行われて、__pycache__フォルダに.pycファイルが作成されます。

.pycファイル中にはコンパイル後のバイトコードが格納されており、.pycファイルが存在するファイルを再度インポートする場合は、コンパイルなしでインポートが可能になります。

.pycファイルを作る例:

%export PYTHONDONTWRITEBYTECODE=0
%python
>>> import lib.demo_mod

実行後のフォルダ構成は以下のようになります。

lib
├── __pycache__
│   └── demo_mod.cpython-313.pyc
└── demo_mod.py

importを行う代わりに以下のコマンドでもコンパイルしてキャッシュの作成がされます。

python -m py_compile lib/demo_mod.py

なお、pipenvなどを使用している場合、PYTHONDONTWRITEBYTECODE=1となっているので、キャッシュができない場合はこの環境変数をみなおしてみてください。

.pycファイルの内容確認

lib/demo_mod.pyのコンパイルしたコードオブジェクトの内容と.pycファイルの関係を検証するコードは以下の通りです。

import pathlib, struct, datetime, os, marshal
import importlib.util


def dump_byte(byte):
    line = '00000000: '
    for i, b in enumerate(byte):
        line += f'{b:02x} '
        if (i+1) % 16 == 0:
            print(line)
            line = f"{(i+1):08x}: "
    print(line)

with open('lib/__pycache__/demo_mod.cpython-313.pyc', 'rb') as f:
    cache_bytes = f.read()
print('demo_mod.cpython-313.pyc ---------------------')
dump_byte(cache_bytes)

# 00000000: f3 0d 0d 0a 
print('pycのMagic number ------------')
dump_byte(cache_bytes[0:4])
print('importlib.util.MAGIC_NUMBER ------------')
dump_byte(importlib.util.MAGIC_NUMBER)                  # b'\xf3\r\r\n'

print('pycのBitField ------------')
dump_byte(cache_bytes[4:8])

st = os.stat("lib/demo_mod.py")

print('pyc中のTimestamp -------')
ts = struct.unpack("<I", cache_bytes[8:12])[0]
dump_byte(cache_bytes[8:12])
print('time stamp', datetime.datetime.fromtimestamp(ts))
print(f"実際のファイルのタイムスタンプ {datetime.datetime.fromtimestamp(st.st_mtime)}")

print('pyc中のFileSize -------')
dump_byte(cache_bytes[12:16])
size = struct.unpack("<I", cache_bytes[12:16])[0]
print("pyc中のFileSize", size)
print(f"実際のファイルのファイルサイズ {st.st_size}")

print('code objectのダンプ---------------------')
# ファイルをコンパイルしてコードオブジェクトのダンプを行う
path = pathlib.Path("lib/demo_mod.py")
src  = path.read_text(encoding="utf-8")

code = compile(src, str(path), "exec")  # ← モジュール本体のコードオブジェクト
dump_byte(marshal.dumps(code))

print("バイトコード.....")
dump_byte(code.co_code)

.pycの全体の内容は以下の通りになります。

00000000: f3 0d 0d 0a 00 00 00 00 1a 36 b9 68 21 00 00 00 
00000010: e3 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 
00000020: 00 00 00 00 00 f3 0a 00 00 00 95 00 53 00 1a 00 
00000030: 72 00 67 01 29 02 63 02 00 00 00 00 00 00 00 00 
00000040: 00 00 00 02 00 00 00 03 00 00 00 f3 0a 00 00 00 
00000050: 95 00 58 01 2d 00 00 00 24 00 29 01 4e a9 00 29 
00000060: 02 da 01 78 da 01 79 73 02 00 00 00 20 20 da 0f 
00000070: 6c 69 62 2f 64 65 6d 6f 5f 6d 6f 64 2e 70 79 da 
00000080: 03 61 64 64 72 07 00 00 00 01 00 00 00 73 09 00 
00000090: 00 00 80 00 d8 0b 0c 89 35 80 4c f3 00 00 00 00 
000000a0: 4e 29 01 72 07 00 00 00 72 03 00 00 00 72 08 00 
000000b0: 00 00 72 06 00 00 00 da 08 3c 6d 6f 64 75 6c 65 
000000c0: 3e 72 09 00 00 00 01 00 00 00 73 0a 00 00 00 f0 
000000d0: 03 01 01 01 f3 02 01 01 11 72 08 00 00 00

00000000〜0000000Fについてはヘッダになっています。
0-4バイトはPythonのバージョンを表すMAGIC_NUMBERになっています。
この値はimportlib.util.MAGIC_NUMBERと一致します。
この例では「f3 0d 0d 0a」となります。

5-8バイトはFlags(Bitfield)となります。これが「00 00 00 00」の場合、タイムスタンプベースのpycとなります。

9-12バイトは元ファイルのタイムスタンプになります。
今回の場合、「1a 36 b9 68」となり「2025-09-04 15:47:54」を表します。

13-15バイトは元ファイルのサイズになります。
今回は「21 00 00 00」となり33バイトを表します。

残りのデータはコンパイル結果のコードオブジェクトをmarshal.dumps関数でダンプした内容と一致します。

以下がコードオブジェクトのダンプ内容で、.pycに格納されていることが確認できます。

00000000: e3 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 
00000010: 00 00 00 00 00 f3 0a 00 00 00 95 00 53 00 1a 00 
00000020: 72 00 67 01 29 02 63 02 00 00 00 00 00 00 00 00 
00000030: 00 00 00 02 00 00 00 03 00 00 00 f3 0a 00 00 00 
00000040: 95 00 58 01 2d 00 00 00 24 00 29 01 4e a9 00 29 
00000050: 02 da 01 78 da 01 79 73 02 00 00 00 20 20 da 0f 
00000060: 6c 69 62 2f 64 65 6d 6f 5f 6d 6f 64 2e 70 79 da 
00000070: 03 61 64 64 72 07 00 00 00 01 00 00 00 73 09 00 
00000080: 00 00 80 00 d8 0b 0c 89 35 80 4c f3 00 00 00 00 
00000090: 4e 29 01 72 07 00 00 00 72 03 00 00 00 72 08 00 
000000a0: 00 00 72 06 00 00 00 da 08 3c 6d 6f 64 75 6c 65 
000000b0: 3e 72 09 00 00 00 01 00 00 00 73 0a 00 00 00 f0 
000000c0: 03 01 01 01 f3 02 01 01 11 72 08 00 00 00

このなかの001Aから始まる「95 00 53 00 1a 00 72 00 67 01」がバイトコードにあたります。

この.pycファイルのフォーマットについては以下のブログが参考になります。
What Is the __pycache__ Folder in Python?

様々なコードのバイトコードの例

分岐のバイトコード

if-elif-elseでの分岐

if-elif-elseのバイトコードを検証します。
今回はjump命令を見やすくするため、show_offsetsでオフセットを付与します。

ソースコード

import dis

def test_branch(a):
    if a == 0:
        return "EMPTY"
    elif a == 1:
        return "ONE"
    else:
        return "OTHER"

dis.dis(test_branch, show_offsets=True)

出力結果

  3          0       RESUME                   0

  4          2       LOAD_FAST                0 (a)
             4       LOAD_CONST               1 (0)
             6       COMPARE_OP              88 (bool(==))
            10       POP_JUMP_IF_FALSE        1 (to L1)

  5         14       RETURN_CONST             2 ('EMPTY')

  6   L1:   16       LOAD_FAST                0 (a)
            18       LOAD_CONST               3 (1)
            20       COMPARE_OP              88 (bool(==))
            24       POP_JUMP_IF_FALSE        1 (to L2)

  7         28       RETURN_CONST             4 ('ONE')

  9   L2:   30       RETURN_CONST             5 ('OTHER')

COMPARE_OPPOP_JUMP_IF_FALSEを使用してif-elif-elseを構築します。

POP_JUMP_IF_FALSEはオペランドで指定したオフセット分余計にジャンプします。

            10       POP_JUMP_IF_FALSE        1 (to L1)

  5         14       RETURN_CONST             2 ('EMPTY')

  6   L1:   16       LOAD_FAST                0 (a)

この例の場合、次のオフセットは14, オペランドは1 * 2byte(オペコードとオペランドのサイズ)であるため、ジャンプ先のオフセットは16になります。
L1とかL2のジャンプ先のラベルはバイトコードに記載があるわけでなく、上記の計算から自動でもとめて、出力時に表示しているだけです。

条件式での分岐

条件式のケースは以下のようになります。

import dis

def test_cond(a):
    return "even" if a % 2 == 0 else "odd"

dis.dis(test_cond, show_offsets=True)

出力結果

  3          0       RESUME                   0

  4          2       LOAD_FAST                0 (a)
             4       LOAD_CONST               1 (2)
             6       BINARY_OP                6 (%)
            10       LOAD_CONST               2 (0)
            12       COMPARE_OP              88 (bool(==))
            16       POP_JUMP_IF_FALSE        2 (to L1)
            20       LOAD_CONST               3 ('even')
            22       RETURN_VALUE
      L1:   24       LOAD_CONST               4 ('odd')
            26       RETURN_VALUE

前述のif-elif-elseと同じくCOMPARE_OPPOP_JUMP_IF_FALSEで実現されていることがわかります。

match文での分岐

match文ではパターンマッチングが行われます。

import dis

def test_match(msg):
    match msg:
        case {"type": "ping"}:
            return "pong"                           # 分岐1: ping
        case {"type": "sum", "args": [x, y]}:
            return x + y                            # 分岐2: sum の二項
        case [x, y, z]:
            return (x, y, z)                        # 分岐3: 長さ3のシーケンス
        case _ if isinstance(msg, int) and msg < 0:
            return "neg"                            # 分岐4: 負の整数(ガード)
        case _:
            return "unknown"                        # 分岐5: それ以外

dis.dis(test_match, show_offsets=True)

バイトコードの出力

  3          0       RESUME                   0

  4          2       LOAD_FAST                0 (msg)

  5          4       COPY                     1
             6       MATCH_MAPPING
             8       POP_JUMP_IF_FALSE       24 (to L3)
            12       GET_LEN
            14       LOAD_CONST               1 (1)
            16       COMPARE_OP             172 (>=)
            20       POP_JUMP_IF_FALSE       18 (to L3)
            24       LOAD_CONST              10 (('type',))
            26       MATCH_KEYS
            28       COPY                     1
            30       POP_JUMP_IF_NONE        11 (to L1)
            34       UNPACK_SEQUENCE          1
            38       LOAD_CONST               2 ('ping')
            40       COMPARE_OP              88 (bool(==))
            44       POP_JUMP_IF_FALSE        5 (to L2)
            48       POP_TOP
            50       POP_TOP
            52       POP_TOP

  6         54       RETURN_CONST             3 ('pong')

  5   L1:   56       POP_TOP
      L2:   58       POP_TOP
      L3:   60       POP_TOP

  7         62       COPY                     1
            64       MATCH_MAPPING
            66       POP_JUMP_IF_FALSE       39 (to L5)
            70       GET_LEN
            72       LOAD_CONST               4 (2)
            74       COMPARE_OP             172 (>=)
            78       POP_JUMP_IF_FALSE       33 (to L5)
            82       LOAD_CONST              11 (('type', 'args'))
            84       MATCH_KEYS
            86       COPY                     1
            88       POP_JUMP_IF_NONE        26 (to L4)
            92       UNPACK_SEQUENCE          2
            96       LOAD_CONST               5 ('sum')
            98       COMPARE_OP              88 (bool(==))
           102       POP_JUMP_IF_FALSE       19 (to L4)
           106       MATCH_SEQUENCE
           108       POP_JUMP_IF_FALSE       16 (to L4)
           112       GET_LEN
           114       LOAD_CONST               4 (2)
           116       COMPARE_OP              72 (==)
           120       POP_JUMP_IF_FALSE       10 (to L4)
           124       UNPACK_SEQUENCE          2
           128       STORE_FAST_STORE_FAST   18 (x, y)
           130       POP_TOP
           132       POP_TOP
           134       POP_TOP

  8        136       LOAD_FAST_LOAD_FAST     18 (x, y)
           138       BINARY_OP                0 (+)
           142       RETURN_VALUE

  7   L4:  144       POP_TOP
           146       POP_TOP
      L5:  148       POP_TOP

  9        150       COPY                     1
           152       MATCH_SEQUENCE
           154       POP_JUMP_IF_FALSE       15 (to L6)
           158       GET_LEN
           160       LOAD_CONST               6 (3)
           162       COMPARE_OP              72 (==)
           166       POP_JUMP_IF_FALSE        9 (to L6)
           170       UNPACK_SEQUENCE          3
           174       STORE_FAST_STORE_FAST   18 (x, y)
           176       STORE_FAST               3 (z)
           178       POP_TOP

 10        180       LOAD_FAST_LOAD_FAST     18 (x, y)
           182       LOAD_FAST                3 (z)
           184       BUILD_TUPLE              3
           186       RETURN_VALUE

  9   L6:  188       POP_TOP

 11        190       POP_TOP
           192       LOAD_GLOBAL              1 (isinstance + NULL)
           202       LOAD_FAST                0 (msg)
           204       LOAD_GLOBAL              2 (int)
           214       CALL                     2
           222       TO_BOOL
           230       POP_JUMP_IF_FALSE        7 (to L7)
           234       LOAD_FAST                0 (msg)
           236       LOAD_CONST               7 (0)
           238       COMPARE_OP              18 (bool(<))
           242       POP_JUMP_IF_FALSE        1 (to L7)

 12        246       RETURN_CONST             8 ('neg')

 13   L7:  248       NOP

 14        250       RETURN_CONST             9 ('unknown')

ifなどの条件分岐に比べて、複雑なバイトコードが作成されましたが1つづつ確認します。

case {"type": "ping"}:は以下のバイトコードに変換されます。

  5          4       COPY                     1
             6       MATCH_MAPPING
             8       POP_JUMP_IF_FALSE       24 (to L3)
            12       GET_LEN
            14       LOAD_CONST               1 (1)
            16       COMPARE_OP             172 (>=)
            20       POP_JUMP_IF_FALSE       18 (to L3)
            24       LOAD_CONST              10 (('type',))
            26       MATCH_KEYS
            28       COPY                     1
            30       POP_JUMP_IF_NONE        11 (to L1)
            34       UNPACK_SEQUENCE          1
            38       LOAD_CONST               2 ('ping')
            40       COMPARE_OP              88 (bool(==))
            44       POP_JUMP_IF_FALSE        5 (to L2)
            48       POP_TOP
            50       POP_TOP
            52       POP_TOP

... caseの中の処理

  5   L1:   56       POP_TOP
      L2:   58       POP_TOP
      L3:   60       POP_TOP

  • MATCH_MAPPINGはSTACK[-1] が辞書か判断します。辞書でなければL3へジャンプして次の判定をします
  • GET_LENCOMPARE_OPで辞書が空ではないか判断します。空の場合はL3へジャンプして次の判定をします。
  • LOAD_CONST〜MATCH_KEYS、指定したキーが全てあるならば、そのキーの値をスタックにプッシュします。ない場合はL1へジャンプして次の判定をします
  • UNPACK_SEQUENCECOMPARE_OPで指定キーの値が一致するかを確認して一致しなければ、L1へジャンプします。もし一致する場合は分岐1の処理を行います。

case {"type": "sum", "args": [x, y]}:は以下のバイトコードに変換されます。

  7         62       COPY                     1
            64       MATCH_MAPPING
            66       POP_JUMP_IF_FALSE       39 (to L5)
            70       GET_LEN
            72       LOAD_CONST               4 (2)
            74       COMPARE_OP             172 (>=)
            78       POP_JUMP_IF_FALSE       33 (to L5)
            82       LOAD_CONST              11 (('type', 'args'))
            84       MATCH_KEYS
            86       COPY                     1
            88       POP_JUMP_IF_NONE        26 (to L4)
            92       UNPACK_SEQUENCE          2
            96       LOAD_CONST               5 ('sum')
            98       COMPARE_OP              88 (bool(==))
           102       POP_JUMP_IF_FALSE       19 (to L4)
           106       MATCH_SEQUENCE
           108       POP_JUMP_IF_FALSE       16 (to L4)
           112       GET_LEN
           114       LOAD_CONST               4 (2)
           116       COMPARE_OP              72 (==)
           120       POP_JUMP_IF_FALSE       10 (to L4)
           124       UNPACK_SEQUENCE          2
           128       STORE_FAST_STORE_FAST   18 (x, y)
           130       POP_TOP
           132       POP_TOP
           134       POP_TOP

... 分岐2の処理

  7   L4:  144       POP_TOP
           146       POP_TOP
      L5:  148       POP_TOP


  • MATCH_KEYS使用した辞書のキーから値を取る処理までは分岐1とほぼ同じです。
  • MATCH_SEQUENCEを使用してargsがシーケンスであることを確認しています。その後、argsの長さを確認していることがわかります。

case [x, y, z]は以下のバイトコードに変換されます。

  9        150       COPY                     1
           152       MATCH_SEQUENCE
           154       POP_JUMP_IF_FALSE       15 (to L6)
           158       GET_LEN
           160       LOAD_CONST               6 (3)
           162       COMPARE_OP              72 (==)
           166       POP_JUMP_IF_FALSE        9 (to L6)
           170       UNPACK_SEQUENCE          3
           174       STORE_FAST_STORE_FAST   18 (x, y)
           176       STORE_FAST               3 (z)
           178       POP_TOP

... 条件3の処理

  9   L6:  188       POP_TOP
  • MATCH_SEQUENCEを使用してシーケンスであること、その長さを確認しています。

case _ if isinstance(msg, int) and msg < 0:は以下のようなバイトコードに変換されます。

 11        190       POP_TOP
           192       LOAD_GLOBAL              1 (isinstance + NULL)
           202       LOAD_FAST                0 (msg)
           204       LOAD_GLOBAL              2 (int)
           214       CALL                     2
           222       TO_BOOL
           230       POP_JUMP_IF_FALSE        7 (to L7)
           234       LOAD_FAST                0 (msg)
           236       LOAD_CONST               7 (0)
           238       COMPARE_OP              18 (bool(<))
           242       POP_JUMP_IF_FALSE        1 (to L7)

 12        246       RETURN_CONST             8 ('neg')
  • 192〜230がisinstanceを呼び出しfalseであれば、POP_JUMP_IF_FALSEで次の判定に移ります
  • 234〜242がandでつづくmsg < 0にあたります。これも条件を満たさない場合はPOP_JUMP_IF_FALSEで次の判定に移ります

ループのバイトコード

for inループのケース

for x in xxxのバイトコードを検証します。

ソースコード

import dis

def test_for(items):
    for item in items:
        if item == "NG":
            break
        if item == "PASS":
            continue
        print(item.lower())
    print("exit loop")

test_for(["AB", "CD", "PASS", "NG", "E"])
dis.dis(test_for, show_offsets=True)

出力結果

  3          0       RESUME                   0

  4          2       LOAD_FAST                0 (items)
             4       GET_ITER
      L1:    6       FOR_ITER                44 (to L4)
            10       STORE_FAST               1 (item)

  5         12       LOAD_FAST                1 (item)
            14       LOAD_CONST               1 ('NG')
            16       COMPARE_OP              88 (bool(==))
            20       POP_JUMP_IF_FALSE        2 (to L2)

  6         24       POP_TOP
            26       JUMP_FORWARD            37 (to L5)

  7   L2:   28       LOAD_FAST                1 (item)
            30       LOAD_CONST               2 ('PASS')
            32       COMPARE_OP              88 (bool(==))
            36       POP_JUMP_IF_FALSE        2 (to L3)

  8         40       JUMP_BACKWARD           19 (to L1)

  9   L3:   44       LOAD_GLOBAL              1 (print + NULL)
            54       LOAD_FAST                1 (item)
            56       LOAD_ATTR                3 (lower + NULL|self)
            76       CALL                     0
            84       CALL                     1
            92       POP_TOP
            94       JUMP_BACKWARD           46 (to L1)

  4   L4:   98       END_FOR
           100       POP_TOP

 10   L5:  102       LOAD_GLOBAL              1 (print + NULL)
           112       LOAD_CONST               3 ('exit loop')
           114       CALL                     1
           122       POP_TOP
           124       RETURN_CONST             0 (None)

基本的な流れはGET_ITERFOR_ITER→繰り返し処理→JUMP_BACKWARD→ループ終了時にEND_FORとなります。

GET_ITERはスタックの最上位のコンテナについてイテレータに変換して、それをスタックに積みます。

FOR_ITERはスタックの最上位のイテレータに対して__next__()メソッドを呼び出します。新しい値が生成されたらスタックに積みます。もしイテレータが終端に達した場合はオペコードの分だけジャンプします。今回のケースではL4のラベルまでジャンプになります。

JUMP_BACKWARDはオペコードに指定した分だけ戻ります。今回のケースだとL1のラベルまでジャンプします。

      L1:    6       FOR_ITER                44 (to L4)
...
            94       JUMP_BACKWARD           46 (to L1)

  4   L4:   98       END_FOR

今回の例だとJUMP_BACKWARDの次のオフセットは98、これに対して46 * 2バイト分戻ります。
すなわちジャンプ先のオフセットは6となり、そのラベルはL1になります。

END_FORはループの終了時にクリーンアップするために使用されます。
POP_TOPと同じ挙動をしてスタックの先頭の項目を削除します。

breakについては以下のコードで実現されています。

  6         24       POP_TOP
            26       JUMP_FORWARD            37 (to L5)

  7   L2:   28       LOAD_FAST                1 (item)
...
 10   L5:  102       LOAD_GLOBAL              1 (print + NULL)
           112       LOAD_CONST               3 ('exit loop')

JUMP_FORWARDはオペランドで指定したぶんだけ増加させてジャンプします。
今回の例ではJUMP_FORWARDの次のオフセットは28, 増分は37 * 2バイト = 74
すなわち、オフセット102のL5の箇所まで飛びます。

continueについては以下のコードで実現されています。

      L1:    6       FOR_ITER                44 (to L4)
...
  8         40       JUMP_BACKWARD           19 (to L1)
  9   L3:   44       LOAD_GLOBAL              1 (print + NULL)

JUMP_BACKWARDでL1に戻るようなジャンプをします。
今回のケースではJUMP_BACKWARDの次のオフセットは44、減分は19 * 2バイト = 38となります。
すなわち、オフセット 48 - 38 = 6へジャンプします。

for i in rangeでのループのケース

for i in range(len(x))のようにインデックスでループする例を見てみます。

import dis

def test_for_range(items):
    for i in range(len(items)):
        item = items[i]
        print(item)

dis.dis(test_for_range, show_offsets=True)

出力結果

  3          0       RESUME                   0

  4          2       LOAD_GLOBAL              1 (range + NULL)
            12       LOAD_GLOBAL              3 (len + NULL)
            22       LOAD_FAST                0 (items)
            24       CALL                     1
            32       CALL                     1
            40       GET_ITER
      L1:   42       FOR_ITER                18 (to L2)
            46       STORE_FAST               1 (i)

  5         48       LOAD_FAST_LOAD_FAST      1 (items, i)
            50       BINARY_SUBSCR
            54       STORE_FAST               2 (item)

  6         56       LOAD_GLOBAL              5 (print + NULL)
            66       LOAD_FAST                2 (item)
            68       CALL                     1
            76       POP_TOP
            78       JUMP_BACKWARD           20 (to L1)

  4   L2:   82       END_FOR
            84       POP_TOP
            86       RETURN_CONST             0 (None)

前述のfor~inのケースと違い明らかに処理が増えています。
GET_ITERを実行する前にrangeとlenの呼び出しが増えています。
また、48 LOAD_FAST_LOAD_FAST, BINARY_SUBSCR, STORE_FASTによる添え字からデータの取得の処理が増えています。

while文でのループのケース

while文でループした場合にどのようなバイトコードが作成されるかを確認してみます。

import dis

def test_while(items):
    i = 0
    while i < len(items):
        item = items[i]
        print(item)
        i += 1
    print("end")

dis.dis(test_while, show_offsets=True)

出力結果

  3          0       RESUME                   0

  4          2       LOAD_CONST               1 (0)
             4       STORE_FAST               1 (i)

  5          6       LOAD_FAST                1 (i)
             8       LOAD_GLOBAL              1 (len + NULL)
            18       LOAD_FAST                0 (items)
            20       CALL                     1
            28       COMPARE_OP              18 (bool(<))
            32       POP_JUMP_IF_FALSE       37 (to L2)

  6   L1:   36       LOAD_FAST_LOAD_FAST      1 (items, i)
            38       BINARY_SUBSCR
            42       STORE_FAST               2 (item)

  7         44       LOAD_GLOBAL              3 (print + NULL)
            54       LOAD_FAST                2 (item)
            56       CALL                     1
            64       POP_TOP

  8         66       LOAD_FAST                1 (i)
            68       LOAD_CONST               2 (1)
            70       BINARY_OP               13 (+=)
            74       STORE_FAST               1 (i)

  5         76       LOAD_FAST                1 (i)
            78       LOAD_GLOBAL              1 (len + NULL)
            88       LOAD_FAST                0 (items)
            90       CALL                     1
            98       COMPARE_OP              18 (bool(<))
           102       POP_JUMP_IF_FALSE        2 (to L2)
           106       JUMP_BACKWARD           37 (to L1)

  9   L2:  110       LOAD_GLOBAL              3 (print + NULL)
           120       LOAD_CONST               3 ('end')
           122       CALL                     1
           130       POP_TOP
           132       RETURN_CONST             0 (None)

while文ではCOMPARE_OPPOP_JUMP_IF_FALSEJUMP_BACKWARDでループを実現していることがわかります。
また、Pythonのコード的にはループの条件は5行目だけで書かれていますが、バイトコードとしてはループ前とループ中の2箇所で生成されていることが確認できます。

enumerateを使用したループ

enumerateを使用してindexと値を同時に取るようなループを試してみます。

import dis

def test_for_enumerate(items):
    for i, item in enumerate(items):
        print(i, item)
    print('end')

dis.dis(test_for_enumerate, show_offsets=True)

出力結果

  3          0       RESUME                   0

  4          2       LOAD_GLOBAL              1 (enumerate + NULL)
            12       LOAD_FAST                0 (items)
            14       CALL                     1
            22       GET_ITER
      L1:   24       FOR_ITER                16 (to L2)
            28       UNPACK_SEQUENCE          2
            32       STORE_FAST_STORE_FAST   18 (i, item)

  5         34       LOAD_GLOBAL              3 (print + NULL)
            44       LOAD_FAST_LOAD_FAST     18 (i, item)
            46       CALL                     2
            54       POP_TOP
            56       JUMP_BACKWARD           18 (to L1)

  4   L2:   60       END_FOR
            62       POP_TOP

  6         64       LOAD_GLOBAL              3 (print + NULL)
            74       LOAD_CONST               1 ('end')
            76       CALL                     1
            84       POP_TOP
            86       RETURN_CONST             0 (None)

GET_ITERの前にCALLでenumerate関数を呼び出しています。
FOR_ITERの直後にUNPACK_SEQUENCEスタックから複数個の値を取得してiとitemとして束縛してます。

例外処理のバイトコード

ここでは基本的な例外処理を含むコードがどのようなバイトコードになるかを確認します。

サンプルコード

import dis

def test_try_catch(f):
    try:
        print('try...')
    except Exception as e:
        print('except', e)
    finally:
        print('finally....')

# offsetを表示する
dis.dis(test_try_catch, show_offsets=True)

出力結果

   3           0       RESUME                   0

   4           2       NOP

   5    L1:    4       LOAD_GLOBAL              1 (print + NULL)
              14       LOAD_CONST               1 ('try...')
              16       CALL                     1
              24       POP_TOP

   9    L2:   26       LOAD_GLOBAL              1 (print + NULL)
              36       LOAD_CONST               3 ('finally....')
              38       CALL                     1
              46       POP_TOP
              48       RETURN_CONST             0 (None)

  --    L3:   50       PUSH_EXC_INFO

   6          52       LOAD_GLOBAL              2 (Exception)
              62       CHECK_EXC_MATCH
              64       POP_JUMP_IF_FALSE       22 (to L7)
              68       STORE_FAST               1 (e)

   7    L4:   70       LOAD_GLOBAL              1 (print + NULL)
              80       LOAD_CONST               2 ('except')
              82       LOAD_FAST                1 (e)
              84       CALL                     2
              92       POP_TOP
        L5:   94       POP_EXCEPT
              96       LOAD_CONST               0 (None)
              98       STORE_FAST               1 (e)
             100       DELETE_FAST              1 (e)
             102       JUMP_BACKWARD_NO_INTERRUPT 39 (to L2)

  --    L6:  104       LOAD_CONST               0 (None)
             106       STORE_FAST               1 (e)
             108       DELETE_FAST              1 (e)
             110       RERAISE                  1

   6    L7:  112       RERAISE                  0

  --    L8:  114       COPY                     3
             116       POP_EXCEPT
             118       RERAISE                  1
        L9:  120       PUSH_EXC_INFO

   9         122       LOAD_GLOBAL              1 (print + NULL)
             132       LOAD_CONST               3 ('finally....')
             134       CALL                     1
             142       POP_TOP
             144       RERAISE                  0

  --   L10:  146       COPY                     3
             148       POP_EXCEPT
             150       RERAISE                  1
ExceptionTable:
  L1 to L2 -> L3 [0]
  L3 to L4 -> L8 [1] lasti
  L4 to L5 -> L6 [1] lasti
  L5 to L6 -> L9 [0]
  L6 to L8 -> L8 [1] lasti
  L8 to L9 -> L9 [0]
  L9 to L10 -> L10 [1] lasti

ExceptionTable

try/except/finallyを含む関数はコードオブジェクトのco_exceptiontableに、ExceptionTableを構築します。
このテーブルは「開始オフセット(含む)と 終了オフセット(含まない)の範囲で例外が起きたら どのハンドラ(ターゲット)へ飛ぶか」に加えて、スタック深さpush-lastiフラグも記録します。

スタック深(角括弧の数値)はハンドラへ入る直前に、値スタックからいくつポップして整えるかを指示します。
例外が発生した時点でのスタック形状は命令ごとに異なるため、ハンドラ開始時に想定の深さにそろえる必要があります。

push-lasti(lasti と表示されるフラグ)はそのエントリに入るとき、例外が発生した命令オフセット(“lasti”)を値スタックに積むかどうかを指定します。
後続で RERAISE 1 を使うと、この lasti を取り出して “元の発生位置にひも付けた再送出” を行えます(トレースバックの位置保持)。
RERAISE 0 は lasti を使わず 現在位置のまま再送出します。

開始オフセット(含む) 終了オフセット(含まない) ターゲット スタックの深さ push-lastiフラグ 説明
L1 L2 L3 0 try区で例外が発生した場合、except区に飛ぶ
L3 L4 L8 1 lasti exceptの型が異なる場合の例外処理
L4 L5 L6 1 lasti except 本体の実行中の例外処理
L5 L6 L9 0 except の後始末コードの途中の例外処理
L6 L8 L8 1 lasti except 節の中や直後の後始末でさらに例外が発生した場合
L8 L9 L9 0 例外処理進行中の場合。(finallyを必ず通すため)
L9 L10 L10 1 lasti 例外後のfinally区のコードでエラーが出た時の例外処理
co_exceptiontableを解析する。

co_exceptiontableのバイナリ形式から前述のようなExceptionTableを構築しています。
Format of the exception tableに、その詳細が記載されています。

co_exceptiontableに格納されているデータは以下で構成されます。

start   (up to 30 bits)
size    (up to 30 bits)
target  (up to 30 bits)
depth   (up to ~8 bits)
lasti   (1 bit)

これを元に前述のようなExceptionTableを構築してます。

具体的に解析を行う場合は、Lib/dis.pyの_parse_exception_tableを参考にするのが望ましいでしょう。

サンプルコード

# https://github.com/python/cpython/blob/3.13/Lib/dis.py#L685
def _parse_exception_table(code):
    iterator = iter(code.co_exceptiontable)
    entries = []
    try:
        while True:
            start = _parse_varint(iterator)*2
            length = _parse_varint(iterator)*2
            end = start + length
            target = _parse_varint(iterator)*2
            dl = _parse_varint(iterator)
            depth = dl >> 1
            lasti = bool(dl&1)
            yield start, end, target, depth, lasti
    except StopIteration:
        return entries

def _parse_varint(iterator):
    b = next(iterator)
    val = b & 63
    while b&64:
        val <<= 6
        b = next(iterator)
        val |= b&63
    return val


for start, end, target, depth, lasti in _parse_exception_table(test_try_catch.__code__):
    print(f"start:{start} end:{end} target:{target}  depth:{depth} lasti:{lasti}")

出力結果

start:4 end:26 target:50  depth:0 lasti:False
start:50 end:70 target:114  depth:1 lasti:True
start:70 end:94 target:104  depth:1 lasti:True
start:94 end:104 target:120  depth:0 lasti:False
start:104 end:114 target:114  depth:1 lasti:True
start:114 end:120 target:120  depth:0 lasti:False
start:120 end:146 target:146  depth:1 lasti:True

例外が発生しない通常経路(try → finally)

下のとおり、try 本体(L1)を実行し、続いて finally 本体(L2)を実行して return None します。例外がなければ ExceptionTable は参照されません(ゼロコスト)

   3           0       RESUME                   0

   4           2       NOP

   5    L1:    4       LOAD_GLOBAL              1 (print + NULL)
              14       LOAD_CONST               1 ('try...')
              16       CALL                     1
              24       POP_TOP

   9    L2:   26       LOAD_GLOBAL              1 (print + NULL)
              36       LOAD_CONST               3 ('finally....')
              38       CALL                     1
              46       POP_TOP
              48       RETURN_CONST             0 (None)

except Exception as e: への遷移と処理

L1〜L2 の範囲(オフセット 4 以上 26 未満)で例外が発生すると、ExceptionTable に従い L3 に入ります。以降は except の型判定・束縛・本体・後始末です。

  --    L3:   50       PUSH_EXC_INFO

   6          52       LOAD_GLOBAL              2 (Exception)
              62       CHECK_EXC_MATCH
              64       POP_JUMP_IF_FALSE       22 (to L7)
              68       STORE_FAST               1 (e)
...
   6    L7:  112       RERAISE                  0
  • PUSH_EXC_INFOを実行します。「STACK[-1] を一時ポップ → 現在の例外(current exception)をプッシュ → さきほどの値を戻す」。これにより、直後の判定が期待する並び(例外本体と型)になります。
  • CHECK_EXC_MATCHSTACK[-2](例外)STACK[-1](Exception型)にマッチするか確認します。STACK[-1](Exception型)をポップしたのちに、マッチの結果の真偽値をスタックに積みます。
  • POP_JUMP_IF_FALSEで指定したラベルL7(112)を使用してスタックの真偽値をチェックしてFALSEであれば、L7にジャンプします。
  • 一致した場合はSTORE_FASTで例外インスタンスを e に束縛します。

続く L4〜L5 は except 本体とクリーンアップです。

   7    L4:   70       LOAD_GLOBAL              1 (print + NULL)
              80       LOAD_CONST               2 ('except')
              82       LOAD_FAST                1 (e)
              84       CALL                     2
              92       POP_TOP
        L5:   94       POP_EXCEPT
              96       LOAD_CONST               0 (None)
              98       STORE_FAST               1 (e)
             100       DELETE_FAST              1 (e)
             102       JUMP_BACKWARD_NO_INTERRUPT 39 (to L2)
  • LOAD_GLOBAL〜POP_TOPがexcept中の命令となります。
  • POP_EXCEPT例外状態を復します。
  • LOAD_CONST〜DELETE_FASTを使用してe のクリーンアップします。
  • JUMP_BACKWARD_NO_INTERRUPTを使用してfinally本体(L2)へ戻ります

例外経路での finally と再送出

  --    L6:  104       LOAD_CONST               0 (None)
             106       STORE_FAST               1 (e)
             108       DELETE_FAST              1 (e)
             110       RERAISE                  1

   6    L7:  112       RERAISE                  0

  --    L8:  114       COPY                     3
             116       POP_EXCEPT
             118       RERAISE                  1
        L9:  120       PUSH_EXC_INFO

   9         122       LOAD_GLOBAL              1 (print + NULL)
             132       LOAD_CONST               3 ('finally....')
             134       CALL                     1
             142       POP_TOP
             144       RERAISE                  0
  --   L10:  146       COPY                     3
             148       POP_EXCEPT
             150       RERAISE                  1
  • L6(104–110)
    • L4〜L5 の範囲(オフセット 70 以上 94未満)で例外が発生すると、ExceptionTable に従い L6 に入ります。
    • LOAD_CONST〜DELETE_FASTで束縛した e を必ずクリーンアップ
    • RERAISE 1で例外をlasti(元の発生位置)を使って再送出する
  • L7(112)
    • except Exception にマッチしなかったケースの経路。
    • RERAISE 0でその場の位置で例外を再送出。
    • ExceptionTable に従って **L9(finally 実行経路)**へ回され、finally を実行してから外側へ伝播する
  • L8(114–118)
    • L3→L4(マッチ判定の途中)や L6→L8(後始末の途中)で再度例外が起きたときの巻き戻し経路
      • COPY 3 ... 再送出に必要な例外情報を一時複製(この後の後始末で失われないようにする)
      • POP_EXCEPT ... ハンドラの入退場状態を復元
      • RERAISE 1で例外をlasti(元の発生位置)を使って再送出する
  • L9(120–144)
    • 例外が進行中でも finally を必ず実行するための専用経路
    • PUSH_EXC_INFO 現在の例外を値スタックに挿入
    • finallyの中身の実行
    • RERAISE 0でその場の位置で例外を再送出。
  • L10(146–150)
    • L9(例外時 finally)の実行中にさらに例外が起きたときの保険経路。
      • COPY 3 ... 再送出に必要な例外情報を一時複製(この後の後始末で失われないようにする)
      • POP_EXCEPT ... ハンドラの入退場状態を復元
      • RERAISE 1で例外をlasti(元の発生位置)を使って再送出する

内包式のバイトコード

map+lambdaの実装は内包式に置き換えることができます。双方のバイトコードでの比較を行います。

import dis

def test_map(items):
    return list(map(lambda item: item * 10, items))

def test_comprehension(items):
    return [item * 10 for item in items ]

print('-------- map')
dis.dis(test_map, show_offsets=True)

print('-------- 内包式')
dis.dis(test_comprehension, show_offsets=True)

mapでのバイトコード

-------- map
  3          0       RESUME                   0

  4          2       LOAD_GLOBAL              1 (list + NULL)
            12       LOAD_GLOBAL              3 (map + NULL)
            22       LOAD_CONST               1 (<code object <lambda> at 0x103ea56f0, file "/work/techblog/python_perfomance/pipenv313/test_byte_code012e1.py", line 4>)
            24       MAKE_FUNCTION
            26       LOAD_FAST                0 (items)
            28       CALL                     2
            36       CALL                     1
            44       RETURN_VALUE

Disassembly of <code object <lambda> at 0x103ea56f0, file "/work/techblog/python_perfomance/pipenv313/test_byte_code012e1.py", line 4>:
  4          0       RESUME                   0
             2       LOAD_FAST                0 (item)
             4       LOAD_CONST               1 (10)
             6       BINARY_OP                5 (*)
            10       RETURN_VALUE
  • 22: LOAD_CONST は ラムダの「コードオブジェクト」を読み込む命令です。
  • 24: MAKE_FUNCTION は、そのコードオブジェクトから 関数オブジェクトを1つ生成します(この時点でデフォルト引数やクロージャは付かない構成です)。本体(item * 10)のバイトコード自体は、すでにラムダ用コードオブジェクトの中にコンパイル済みであり、MAKE_FUNCTION が「中身を作る」わけではありません。

このフレームで見える関数呼び出し(CALL)は次の2回です。

  • map(lambda, items) を呼ぶ
  • その戻り値(イテレータ)を list(...) に渡す

加えて、実行時には list(...) が map のイテレータを走査するため、入力要素ごとにラムダ関数が1回ずつ呼び出されます(各呼び出しはラムダ側のフレームで RESUME … RETURN_VALUE が実行されます)

内包式でのバイトコード

-------- 内包式
   6          0       RESUME                   0

   7          2       LOAD_FAST                0 (items)
              4       GET_ITER
              6       LOAD_FAST_AND_CLEAR      1 (item)
              8       SWAP                     2
       L1:   10       BUILD_LIST               0
             12       SWAP                     2
             14       GET_ITER
       L2:   16       FOR_ITER                 7 (to L3)
             20       STORE_FAST_LOAD_FAST    17 (item, item)
             22       LOAD_CONST               1 (10)
             24       BINARY_OP                5 (*)
             28       LIST_APPEND              2
             30       JUMP_BACKWARD            9 (to L2)
       L3:   34       END_FOR
             36       POP_TOP
       L4:   38       SWAP                     2
             40       STORE_FAST               1 (item)
             42       RETURN_VALUE

  --   L5:   44       SWAP                     2
             46       POP_TOP

   7         48       SWAP                     2
             50       STORE_FAST               1 (item)
             52       RERAISE                  0
ExceptionTable:
  L1 to L4 -> L5 [2]

内包式を使用した場合、ExceptionTableが作成されます。
これは内包式の本体区間で例外が出た場合にクリーンアップ処理を担当します。
たとえば、関数本体にcountという内包式の中で使用している変数があっても、それに影響を与えないようにする必要があります。

まず、オフセット2〜8ではイテレータ作成と変数退避を行います。

   7          2       LOAD_FAST                0 (items)
              4       GET_ITER
              6       LOAD_FAST_AND_CLEAR      1 (item)
              8       SWAP                     2
  • LOAD_FAST_AND_CLEARSWAPで内包式の外側にあるitemという名前があれば退避しておきます。

L1の処理では結果リストを作りループを開始します。

       L1:   10       BUILD_LIST               0
             12       SWAP                     2
             14       GET_ITER

反復処理の本体は以下になります。

       L2:   16       FOR_ITER                 7 (to L3)
             20       STORE_FAST_LOAD_FAST    17 (item, item)
             22       LOAD_CONST               1 (10)
             24       BINARY_OP                5 (*)
             28       LIST_APPEND              2
             30       JUMP_BACKWARD            9 (to L2)

通常のFOR_ITERを使用したループ処理とほぼ同じです。

ループ終了後に後始末を実施して、呼び出し元に値をかえします。

       L3:   34       END_FOR
             36       POP_TOP
       L4:   38       SWAP                     2
             40       STORE_FAST               1 (item)
             42       RETURN_VALUE

反復処理で例外が出た場合、以下の処理を実行して後始末をします。

  --   L5:   44       SWAP                     2
             46       POP_TOP

   7         48       SWAP                     2
             50       STORE_FAST               1 (item)
             52       RERAISE                  0

ジェネレーターのバイトコード

ジェネレータを使用する関数のバイトコードを確認してみます。

import dis

def inner_func1():
    print("start....")
    x = yield 1
    yield x + 1

def outer_func():
    yield from inner_func1()

print('----------- inner_func1')
dis.dis(inner_func1, show_offsets=True)

print('----------- outer_func')
dis.dis(outer_func, show_offsets=True)

print("test...")
it = outer_func()
print("next1...")
print(next(it))
print("next2...")
print(it.send(10))

yieldを使用した関数の場合

inner_func1のバイトコードは以下のようになります。

----------- inner_func1
   3          0       RETURN_GENERATOR
              2       POP_TOP
       L1:    4       RESUME                   0

   4          6       LOAD_GLOBAL              1 (print + NULL)
             16       LOAD_CONST               1 ('start....')
             18       CALL                     1
             26       POP_TOP

   5         28       LOAD_CONST               2 (1)
             30       YIELD_VALUE              0
             32       RESUME                   5
             34       STORE_FAST               0 (x)

   6         36       LOAD_FAST                0 (x)
             38       LOAD_CONST               2 (1)
             40       BINARY_OP                0 (+)
             44       YIELD_VALUE              0
             46       RESUME                   5
             48       POP_TOP
             50       RETURN_CONST             0 (None)

  --   L2:   52       CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)
             54       RERAISE                  1
ExceptionTable:
  L1 to L2 -> L2 [0] lasti

yieldを使用した関数についてはExceptionTableが生成されます。
ExcepetionTableで保護された区間、RESUMEを実行する際はexcept-depth 1となります。

まず初回の再開ポイントまでのバイトコードを確認します。

   3          0       RETURN_GENERATOR
              2       POP_TOP
       L1:    4       RESUME                   0
  • RETURN_GENERATORは現在のフレームからジェネレータ、コルーチン、または非同期ジェネレータを作成します。RETURN_GENERATORを持つ関数を実行した場合、関数の本体は即時実行されません。返したジェネレータに対してnext()やsend()をした場合に再開点から動きます。
  • RESUME 0は、初回の再開点になります。

1回目のyieldのバイトコードを確認します。

   5         28       LOAD_CONST               2 (1)
             30       YIELD_VALUE              0
             32       RESUME                   5
             34       STORE_FAST               0 (x)
  • YIELD_VALUE 0STACK[-1]を呼び出し側へ返し(yield)、フレームを一時停止します。オペランド0は通常のYIELDであることを表します。
  • RESUME 5
    • フレームが再開されたときの最初のマーカーを表します。
    • 5の意味は「yield 直後」かつ「except-depth 1」という意味になります。
      • 下位2bit 01: yield 直後
      • 次bit except-depthのフラグ
  • STORE_FASTで送信値を受け取る
    • ジェネレーターにたいしてsend()を実行した場合、STACK[-1]に値が格納されているのでそれを取り出してxに格納する
    • next()の場合はNoneになる

2回目のyieldのバイトコードを確認します。

   6         36       LOAD_FAST                0 (x)
             38       LOAD_CONST               2 (1)
             40       BINARY_OP                0 (+)
             44       YIELD_VALUE              0
             46       RESUME                   5
             48       POP_TOP
  • YIELD_VALUERESUME 5は1回目と同じです。
  • 今回はsendで外部からのデータを受け取らないため、POP_TOPSTACK[-1]を無視します。

最後に例外処理について確認します。

  --   L2:   52       CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)
             54       RERAISE                  1
  • L1〜L2 の範囲で例外が発生したら実行されます。
  • CALL_INTRINSIC_1でINTRINSIC_STOPITERATION_ERRORを実行します。StopIterationからvalueを抽出してスタックに積みます
  • RERAISE 1で例外をlasti(元の発生位置)を使って再送出する

yield fromを使用した関数の場合

outer_funcのバイトコードは以下のようになります。

----------- outer_func
   8          0       RETURN_GENERATOR
              2       POP_TOP
       L1:    4       RESUME                   0

   9          6       LOAD_GLOBAL              1 (inner_func1 + NULL)
             16       CALL                     0
             24       GET_YIELD_FROM_ITER
             26       LOAD_CONST               0 (None)
       L2:   28       SEND                     3 (to L5)
       L3:   32       YIELD_VALUE              1
       L4:   34       RESUME                   2
             36       JUMP_BACKWARD_NO_INTERRUPT 5 (to L2)
       L5:   38       END_SEND
             40       POP_TOP
             42       RETURN_CONST             0 (None)
       L6:   44       CLEANUP_THROW
       L7:   46       JUMP_BACKWARD_NO_INTERRUPT 5 (to L5)

  --   L8:   48       CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)
             50       RERAISE                  1
ExceptionTable:
  L1 to L3 -> L8 [0] lasti
  L3 to L4 -> L6 [2]
  L4 to L7 -> L8 [0] lasti

オフセット6からのLOAD_GLOBAL、CALL、GET_YIELD_FROM_ITERで委譲先(inner_func1)のイテレータを作成します。

委譲先イテレータのループ処理は以下のように行います。

             26       LOAD_CONST               0 (None)
       L2:   28       SEND                     3 (to L5)
       L3:   32       YIELD_VALUE              1
       L4:   34       RESUME                   2
             36       JUMP_BACKWARD_NO_INTERRUPT 5 (to L2)
...
  • SENDで委譲先にSTACK[-1]送る(初回は0)
    • 委譲先が終了(StopIteration)したら、引数のラベル L5 END_SENDにジャンプする
    • 委譲先が値を返したらYIELD_VALUE 1
  • YIELD_VALUE 1STACK[-1]を呼び出し側へ返し(yield)、フレームを一時停止します。オペランド1は通常のyield fromであることを表します。
  • RESUME 2はyield fromの直後の再開ポイントであることを表します。
  • JUMP_BACKWARD_NO_INTERRUPTでL2に戻ります。

ループが終了したら以下のようにクリーンアップをして呼び出し元に帰ります。

       L5:   38       END_SEND
             40       POP_TOP
             42       RETURN_CONST             0 (None)

最後にExceptionTableを確認します。

開始オフセット(含む) 終了オフセット(含まない) ターゲット スタックの深さ push-lastiフラグ 説明
L1 L3 L8 0 lasti 委譲前に例外が発生した場合はL8へジャンプする
L3 L4 L6 2 委譲先での例外が発生した場合はL6へジャンプする
L4 L7 L8 0 lasti RESUME 直後のループ復帰、END_SEND やクリーンアップ経路で例外が出たらL8へジャンプする

以下が例外の先になります

       L6:   44       CLEANUP_THROW
       L7:   46       JUMP_BACKWARD_NO_INTERRUPT 5 (to L5)

  --   L8:   48       CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)
             50       RERAISE                  1
  • L6

  • L8

    • CALL_INTRINSIC_1でINTRINSIC_STOPITERATION_ERRORを実行します。StopIterationからvalueを抽出してスタックに積みます
    • RERAISE 1で例外をlasti(元の発生位置)を使って再送出します。

非同期関数のバイトコード

async, awaitを使用した場合のバイトコードについて確認します。

import dis
import asyncio

async def test_async():
    await asyncio.sleep(10)
    return 10
 
dis.dis(test_async, show_offsets=True)

出力結果

   4          0       RETURN_GENERATOR
              2       POP_TOP
       L1:    4       RESUME                   0

   5          6       LOAD_GLOBAL              0 (asyncio)
             16       LOAD_ATTR                2 (sleep)
             36       PUSH_NULL
             38       LOAD_CONST               1 (10)
             40       CALL                     1
             48       GET_AWAITABLE            0
             50       LOAD_CONST               0 (None)
       L2:   52       SEND                     3 (to L5)
       L3:   56       YIELD_VALUE              1
       L4:   58       RESUME                   3
             60       JUMP_BACKWARD_NO_INTERRUPT 5 (to L2)
       L5:   62       END_SEND
             64       POP_TOP

   6         66       RETURN_CONST             1 (10)

   5   L6:   68       CLEANUP_THROW
       L7:   70       JUMP_BACKWARD_NO_INTERRUPT 5 (to L5)

  --   L8:   72       CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)
             74       RERAISE                  1
ExceptionTable:
  L1 to L3 -> L8 [0] lasti
  L3 to L4 -> L6 [2]
  L4 to L7 -> L8 [0] lasti

まず最初のバイトコードを確認します。非同期関数のバイトコードはジェネレータを使用する関数と同じであることがわかります。

   4          0       RETURN_GENERATOR
              2       POP_TOP
       L1:    4       RESUME                   0
  • RETURN_GENERATORは現在のフレームからジェネレータ、コルーチン、または非同期ジェネレータを作成します。RETURN_GENERATORを持つ関数を実行した場合、関数の本体は即時実行されません。今回のケースではawaitやasyncio.runなどで実行する必要などがあります。
  • RESUME 0は、再開点になります。

次にawait asyncio.sleep()について確認します。

   5          6       LOAD_GLOBAL              0 (asyncio)
             16       LOAD_ATTR                2 (sleep)
             36       PUSH_NULL
             38       LOAD_CONST               1 (10)
             40       CALL                     1
             48       GET_AWAITABLE            0
  • LOAD_GLOBAL〜CALLでasyncio.sleep(10)を呼び出します。
  • この返却値をGET_AWAITABLEでawait 可能(コルーチン/await 実装) であることを確認し、待機ループで使える形(イテレータ)に正規化します

待機のためのループは以下の通りです。yield-fromのバイトコードに類似していることが確認できます。

             50       LOAD_CONST               0 (None)
       L2:   52       SEND                     3 (to L5)
       L3:   56       YIELD_VALUE              1
       L4:   58       RESUME                   3
             60       JUMP_BACKWARD_NO_INTERRUPT 5 (to L2)
  • SENDで委譲先にSTACK[-1]送る(初回は0)
    • 委譲先が終了(StopIteration)したら、引数のラベル L5 END_SENDにジャンプする
    • 委譲先が値を返したらYIELD_VALUE 1
  • YIELD_VALUE 1STACK[-1]を呼び出し側へ返し(yield)、フレームを一時停止します。オペランド1は通常のyield fromであることを表します。
  • RESUME 3はawait直後の再開ポイントであることを表します。
  • JUMP_BACKWARD_NO_INTERRUPTでL2に戻ります。

例外処理や、待機終了後のクリーンアップ処理もyield-fromに類似していることが確認できます。

大量の変数を宣言したケース

大量に変数や定数を持つ時にオペランドは1バイトで表現できないケースがあります。
この例を確認します.

import dis

# 引数を 257 個もつ関数を動的生成(a0, a1, ..., a256)
n = 257
args = ", ".join(f"a{i}" for i in range(n))
src = f"def f({args}):\n    return a{n-1}\n"

ns = {}
exec(src, ns)
f = ns["f"]

dis.dis(f, show_offsets=True)

出力

  1          0       RESUME                   0

  2          2       EXTENDED_ARG             1
             4       LOAD_FAST_CHECK        256 (a256)
             6       RETURN_VALUE

EXTENDED_ARGを使用することで直後のオペコードのオペランドを1バイトを超えて拡張できます。
今回はco_varnames[256]にアクセスするために使用されています。

参考

Discussion