Jaybanuan's Blog

どうせまた調べるハメになることをメモしていくブログ

Pythonのスタックフレームを雑に表示する

はじめに

Pythonの理解を深めるために、スタックフレームからどんな情報が得られるかを調査した。 思いの外大量の情報が得られてカオスになったので、目視できる程度の情報を雑にprint()で表示してみた。

環境

$ python3 --version
Python 3.10.12

コード

コードが雑なので、説明も雑に。。。 スタックフレームのオブジェクトが持つインスタンス変数を表示する。 直接関係ない情報や大量に表示される情報(特に変数とバイトコード)もあるので、ある程度絞り込みを行っている。 中には無限再帰してしまうものもあったので、そういうものは適当に除外している。

import inspect
import json
import collections.abc

from typing import Any


exclude_preds = {
    'frame': lambda name, value: (not name.startswith('f_')) or (name in ['f_back', 'f_builtins', 'f_globals', 'f_locals']),
    'f_code': lambda name, value: (not name.startswith('co_')) or (name in ['co_varnames', 'co_cellvars', 'co_freevars', 'co_code', 'co_consts', 'co_names', 'co_lnotab', 'co_linetable'])
}


def default_exclude_pred(name: str, value: Any) -> bool:
    return name.startswith('_')


def __collect_stackframe_details(name: str | None, value: Any) -> None | bool | int | float | str | list | dict:
    if isinstance(value, None | bool | int | float | str):
        return value
    elif isinstance(value, list):
        return [__collect_stackframe_details(None, entry) for entry in value]
    else:
        result: dict[str, Any] = {}

        exclude_pred = exclude_preds[name] if name in exclude_preds else default_exclude_pred
        for n, v in (value.items() if isinstance(value, collections.abc.Mapping) else inspect.getmembers(value)):
            if exclude_pred(n, v):
                continue

            result[n] = __collect_stackframe_details(n, v)

        return result


def collect_stackframe_details() -> list:
    result = []

    for stack_frame in inspect.stack():
        result.append(__collect_stackframe_details(None, stack_frame))

    return result


print(json.dumps(collect_stackframe_details(), indent=4))

実行結果

$ python3 stack_frame_details.py
[
    {
        "code_context": [
            "    for stack_frame in inspect.stack():\n"
        ],
        "count": {},
        "filename": "/home/jaybanuan/src/stack_frame_details/stack_frame_ditails.py",
        "frame": {
            "f_code": {
                "co_argcount": 0,
                "co_filename": "/home/jaybanuan/src/stack_frame_details/stack_frame_ditails.py",
                "co_firstlineno": 36,
                "co_flags": 67,
                "co_kwonlyargcount": 0,
                "co_lines": {},
                "co_name": "collect_stackframe_details",
                "co_nlocals": 2,
                "co_posonlyargcount": 0,
                "co_stacksize": 6
            },
            "f_lasti": 26,
            "f_lineno": 40,
            "f_trace": null,
            "f_trace_lines": true,
            "f_trace_opcodes": false
        },
        "function": "collect_stackframe_details",
        "index": 0,
        "lineno": 39
    },
    {
        "code_context": [
            "print(json.dumps(collect_stackframe_details(), indent=4))        \n"
        ],
        "count": {},
        "filename": "/home/jaybanuan/src/stack_frame_details/stack_frame_ditails.py",
        "frame": {
            "f_code": {
                "co_argcount": 0,
                "co_filename": "/home/jaybanuan/src/stack_frame_details/stack_frame_ditails.py",
                "co_firstlineno": 1,
                "co_flags": 64,
                "co_kwonlyargcount": 0,
                "co_lines": {},
                "co_name": "<module>",
                "co_nlocals": 0,
                "co_posonlyargcount": 0,
                "co_stacksize": 7
            },
            "f_lasti": 148,
            "f_lineno": 45,
            "f_trace": null,
            "f_trace_lines": true,
            "f_trace_opcodes": false
        },
        "function": "<module>",
        "index": 0,
        "lineno": 45
    }
]

実はパッケージの情報が欲しかった

スタックフレームの中身を確認したかったのは、各スタックフレームがどのパッケージ由来なのかを知りたかったのだが、直接的な情報を見つけられなかった。 手元では試してないが、すごく頑張って似たような情報を構築している例を見つけた。

stackoverflow.com

参考

github.com

docs.python.org