你好,哈布尔!
今天,我们将以简短的形式分析 CPython 内部发生的事情,当函数相互调用时:、、、,以及创建我们自己的装饰器。sys._getframe
f_back
f_globals
f_locals
CPython 中调用堆栈的内部结构
当您在 Python 中调用函数时,解释器会创建一个 .此对象可以与程序执行日志中的页面进行比较,该页面记录了有关当前调用的所有信息。让我们考虑一下每个此类帧中存储的内容:**frame**
-
函数名称和源代码:
-
f_code
是一个包含字节码、函数名称、文件名和其他元信息的对象。正是通过此字段,您可以找出当前正在运行的函数,并在必要时访问其源代码。 -
当前行号**:**
此值指示当前正在运行的源代码行。如果您曾经调试过代码并试图找出错误发生的确切位置,则此数字可能是一个线索。f_lineno
-
Local and Global Variable Dictionaries**:**
这些字典包含当前可用的所有变量。Local Variables 是在函数中定义的变量,而全局变量是整个模块的通用变量。f_locals
f_global
-
Previous Frame Reference(上一帧引用**):**
这是对堆栈上上一个调用的引用。每一帧都是一个纸质注释,上面写着“I was called from this other note”(我被从另一个注释中调用)。通过链接允许您通过“向后”移动来重建从当前函数到程序入口点的整个调用路径。f_back
f_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()
while
frame.f_code.co_name
frame.f_lineno
stack
frame.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_main
runcode
``launch_instance
run_forever
foo
bar
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_function
traced_function
another_traced_function
write
ismaster_process
scheduleflush
有权访问调用上下文的装饰器
如果您不仅需要包装一个函数,还需要找出谁调用了它,该怎么办?这就是 .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 在线课程中掌握:在目录中,您可以看到所有程序的列表,并且在 日历中您可以注册公开课程。