在 Python 中使用 sys._getframe 进行调试

你好,哈布尔!

今天,我们将以简短的形式分析 CPython 内部发生的事情,当函数相互调用时:、、、,以及创建我们自己的装饰器。sys._getframef_backf_globalsf_locals

CPython 中调用堆栈的内部结构

当您在 Python 中调用函数时,解释器会创建一个 .此对象可以与程序执行日志中的页面进行比较,该页面记录了有关当前调用的所有信息。让我们考虑一下每个此类帧中存储的内容:**frame**

  • 函数名称和源代码:

  • f_code是一个包含字节码、函数名称、文件名和其他元信息的对象。正是通过此字段,您可以找出当前正在运行的函数,并在必要时访问其源代码。

  • 当前行号**:**
    此值指示当前正在运行的源代码行。如果您曾经调试过代码并试图找出错误发生的确切位置,则此数字可能是一个线索。f_lineno

  • Local and Global Variable Dictionaries**:**
    这些字典包含当前可用的所有变量。Local Variables 是在函数中定义的变量,而全局变量是整个模块的通用变量。f_localsf_global

  • Previous Frame Reference(上一帧引用**):**
    这是对堆栈上上一个调用的引用。每一帧都是一个纸质注释,上面写着“I was called from this other note”(我被从另一个注释中调用)。通过链接允许您通过“向后”移动来重建从当前函数到程序入口点的整个调用路径。f_backf_back

让我们考虑如何遍历调用的所有帧:

import sys

def print_call_stack():
    frame = sys._getframe()  # Получаем текущий кадр (функция print_call_stack)
    stack = []
    while frame:
        # Формируем строку: имя функции и текущая строка в коде
        stack.append(f"{frame.f_code.co_name} (line {frame.f_lineno})")
        # Переходим к предыдущему кадру
        frame = frame.f_back
    print("Call Stack (от текущего к началу):")
    # Выводим стек в обратном порядке (от самой верхней точки входа до текущей функции)
    for entry in reversed(stack):
        print("  ->", entry)

def foo():
    bar()

def bar():
    print_call_stack()

foo()

该函数返回当前帧,即执行 的帧,例如,作为形成调用链的起点;然后,在一个循环中,当帧存在时,我们检索函数名称 through 和行号 by,将此信息添加到列表中,并在循环完成后继续前一帧,我们以相反的顺序输出堆栈,获取从根调用(入口点)到当前调用的序列,其中最上面的元素是执行的开始,最低的元素是最近的调用。sys._getframe()print_call_stack()whileframe.f_code.co_nameframe.f_linenostackframe.f_back;

假设在运行此脚本时获得了以下输出:

Call Stack (от текущего к началу):
  -> _run_module_as_main (line 198)
  -> _run_code (line 88)
  ->  (line 37)
  -> launch_instance (line 992)
  -> start (line 712)
  -> start (line 205)
  -> run_forever (line 608)
  -> _run_once (line 1936)
  -> _run (line 84)
  -> dispatch_queue (line 510)
  -> process_one (line 499)
  -> dispatch_shell (line 406)
  -> execute_request (line 730)
  -> do_execute (line 383)
  -> run_cell (line 528)
  -> run_cell (line 2975)
  -> _run_cell (line 3030)
  -> _pseudo_sync_runner (line 78)
  -> run_cell_async (line 3257)
  -> run_ast_nodes (line 3473)
  -> run_code (line 3553)
  ->  (line 19)
  -> foo (line 14)
  -> bar (line 17)
  -> print_call_stack (line 7)

这是一个完整的调用链,从最低级的解释器函数(例如,、、)、开始,通过环境服务调用(以及特定于交互式 shell(如 Jupyter/IPython)的其他调用),再到您的代码,您可以在其中看到函数(第 14 行)调用(第 17 行),而该函数又调用(第 7 行);此输出允许您查看系统如何组织代码的执行,并有助于本地化调用发生的阶段 (和上下文)。runmodule_as_mainruncode``launch_instancerun_foreverfoobar print_call_stack

构建您自己的跟踪器:不使用 PDB 进行调试

现在,让我们创建一个轻量级调试器来跟踪所有函数调用。为此,Python 有一个函数,允许您设置全局事件处理程序。sys.settrace

简单的 Call 和 Return Tracer:

import sys

def simple_tracer(frame, event, arg):
    if event == "call":
        code = frame.f_code
        func_name = code.co_name
        line_no = frame.f_lineno
        print(f"[CALL] {func_name} at line {line_no}")
    elif event == "return":
        code = frame.f_code
        func_name = code.co_name
        print(f"[RETURN] {func_name} returning {arg}")
    return simple_tracer

def traced_function(x):
    return x * 2

def another_traced_function(y):
    result = traced_function(y)
    return result + 1

def run_tracer():
    sys.settrace(simple_tracer)
    print("Result:", another_traced_function(5))
    sys.settrace(None)

run_tracer()

结果,我们得到以下代码:

[CALL] another_traced_function at line 18
[CALL] traced_function at line 15
[RETURN] traced_function returning 10
[RETURN] another_traced_function returning 11
[CALL] write at line 526
[CALL] _is_master_process at line 437
[RETURN] _is_master_process returning True
Result:[CALL] _schedule_flush at line 456
[RETURN] _schedule_flush returning None
[RETURN] write returning 7
[CALL] write at line 526
[CALL] _is_master_process at line 437
[RETURN] _is_master_process returning True
 [CALL] _schedule_flush at line 456
[RETURN] _schedule_flush returning None
[RETURN] write returning 1
[CALL] write at line 526
[CALL] _is_master_process at line 437
[RETURN] _is_master_process returning True
11[CALL] _schedule_flush at line 456
[RETURN] _schedule_flush returning None
[RETURN] write returning 2
[CALL] write at line 526
[CALL] _is_master_process at line 437
[RETURN] _is_master_process returning True

[CALL] _schedule_flush at line 456
[RETURN] _schedule_flush returning None
[RETURN] write returning 1

Tracer 不仅拦截来自用户代码的函数调用和返回,还拦截来自启动的内部系统调用,例如,在打印结果时。您可以看到,函数(第 18 行)首先被调用,该函数在内部调用(第 15 行);它返回值 10,然后返回 11。然后,当通过打印输出结果时,将启动其他内部调用:函数 、 、 ,这些函数负责处理输出并将其同步到控制台。another_traced_functiontraced_functionanother_traced_functionwriteismaster_processscheduleflush

有权访问调用上下文的装饰器

如果您不仅需要包装一个函数,还需要找出谁调用了它,该怎么办?这就是 .sys._getframe

import sys
from functools import wraps

def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Получаем кадр вызывающей функции
        caller_frame = sys._getframe(1)
        caller_name = caller_frame.f_code.co_name
        caller_line = caller_frame.f_lineno
        print(f"[LOG] Функция '{func.__name__}' вызвана из '{caller_name}' на строке {caller_line}")
        result = func(*args, **kwargs)
        print(f"[LOG] Функция '{func.__name__}' завершилась с результатом {result}")
        return result
    return wrapper

@log_call
def compute_area(radius):
    from math import pi
    return pi * radius ** 2

def main():
    area = compute_area(5)
    print(f"Площадь круга: {area}")

main()

推理:

[LOG] Функция 'compute_area' вызвана из 'main' на строке 23
[LOG] Функция 'compute_area' завершилась с результатом 78.53981633974483
Площадь круга: 78.53981633974483

有时,您不希望始终启用跟踪,而只在某些条件下启用跟踪。例如,如果第一个参数超过指定值:

def conditional_trace(threshold):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Если первый аргумент — число и превышает порог, включаем трассировку
            if args and isinstance(args[0], (int, float)) and args[0] > threshold:
                print(f"[TRACE] {func.__name__} вызвана с args={args} kwargs={kwargs}")
                caller_frame = sys._getframe(1)
                print(f"[TRACE] Вызвана из {caller_frame.f_code.co_name} на строке {caller_frame.f_lineno}")
            result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@conditional_trace(10)
def multiply(a, b):
    return a * b

def test():
    print(multiply(5, 3))   # Трассировка не сработает, 5  10

test()

推理:

15
[TRACE] multiply вызвана с args=(15, 2) kwargs={}
[TRACE] Вызвана из test на строке 21
30

基于 sys._getframe 的轻量级分析器

如果您不仅想调试代码,还想分析其执行情况,那么我们将组装一个轻量级分析器,该分析器将测量函数的时间并显示它们的调用位置。

import sys
import time
from functools import wraps

def profile(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        # Извлекаем информацию о вызывающем контексте
        caller = sys._getframe(1)
        caller_info = f"{caller.f_code.co_name} (line {caller.f_lineno})"
        print(f"[PROFILE] Функция '{func.__name__}' вызвана из {caller_info} заняла {end_time - start_time:.6f} секунд")
        return result
    return wrapper

@profile
def heavy_computation(n):
    s = 0
    for i in range(n):
        s += i ** 2
    return s

def run_computation():
    result = heavy_computation(100000)
    print("Результат вычислений:", result)

run_computation()

推理:

[PROFILE] Функция 'heavy_computation' вызвана из run_computation (line 26) заняла 0.011234 секунд
Результат вычислений: 333328333350000

当然,这不仅仅是关于调用堆栈。在评论中分享您的经验。

所有当前的最佳编程实践都可以在 OTUS 在线课程中掌握:在目录中,您可以看到所有程序的列表,并且在 日历中您可以注册公开课程。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇