PEP 553 – 内置 breakpoint()
- 作者:
- Barry Warsaw <barry at python.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2017年9月5日
- Python 版本:
- 3.7
- 发布历史:
- 2017年9月5日,2017年9月7日,2017年9月13日
- 决议:
- Python-Dev 消息
摘要
本 PEP 提议添加一个新的内置函数 breakpoint()
,该函数在调用点进入 Python 调试器。此外,sys
模块中添加了两个新名称,以使进入哪个调试器可配置。
基本原理
Python 的标准库中长期以来都有一个很棒的调试器,名为 pdb
。设置断点通常这样写:
foo()
import pdb; pdb.set_trace()
bar()
因此,在执行 foo()
之后和执行 bar()
之前,Python 将进入调试器。然而,这种惯用法有几个缺点。
- 输入量大(27 个字符)。
- 容易打错字。PEP 作者经常打错这一行,例如省略分号,或者输入点而不是下划线。
- 它将调试直接与 pdb 的选择绑定。可能还有其他调试选项,比如你正在使用 IDE 或其他开发环境。
- Python 代码检查工具(例如 flake8 [代码检查工具])会抱怨这一行,因为它包含两个语句。将这种惯用法分解成两行会使其使用复杂化,因为在清理时更容易出错。也就是说,当你不再需要调试代码时,你可能会忘记删除其中一行。
Python 开发者还有许多其他调试器可供选择,但记住如何调用它们可能很麻烦。例如,即使 IDE 有用于设置断点的用户界面,直接编辑代码可能仍然更方便。以编程方式进入调试器的 API 不一致,因此很难记住具体要输入什么。
通过提供一个通用的 API 来进入调试器,如本 PEP 中所提议的,我们可以解决所有这些问题。
提案
JavaScript 语言提供了一个 debugger
语句 [js-debugger],它在语句出现的位置进入调试器。
本 PEP 提议一个新的内置函数 breakpoint()
,它在调用位置进入 Python 调试器。因此,上面的例子将这样写:
foo()
breakpoint()
bar()
此外,本 PEP 提议为 sys
模块引入两个新的名称绑定,名为 sys.breakpointhook()
和 sys.__breakpointhook__
。默认情况下,sys.breakpointhook()
实现实际的导入和进入 pdb.set_trace()
,它可以设置为不同的函数来改变 breakpoint()
进入的调试器。
sys.__breakpointhook__
被初始化为与 sys.breakpointhook()
相同的函数,这样你就可以总是轻松地将 sys.breakpointhook()
重置为默认值(例如,通过执行 sys.breakpointhook = sys.__breakpointhook__
)。这与现有的 sys.displayhook()
/ sys.__displayhook__
和 sys.excepthook()
/ sys.__excepthook__
的工作方式完全相同 [钩子]。
内置函数的签名是 breakpoint(*args, **kws)
。位置参数和关键字参数直接传递给 sys.breakpointhook()
,签名必须匹配,否则将引发 TypeError
。从 sys.breakpointhook()
返回的值会传递回 breakpoint()
并作为其返回值。
此原理基于以下观察:底层调试器可能接受额外的可选参数。例如,IPython 允许您指定一个字符串,该字符串在进入断点时打印 [ipython-embed]。截至 Python 3.7,pdb 模块也支持可选的 header
参数 [pdb-header]。
环境变量
sys.breakpointhook()
的默认实现会检查一个新的环境变量 PYTHONBREAKPOINT
。这个环境变量可以有各种值:
PYTHONBREAKPOINT=0
禁用调试。具体来说,当设置为这个值时,sys.breakpointhook()
会立即返回None
。PYTHONBREAKPOINT=
(即空字符串)。这与根本不设置环境变量相同,在这种情况下,pdb.set_trace()
照常运行。PYTHONBREAKPOINT=some.importable.callable
。在这种情况下,sys.breakpointhook()
会导入some.importable
模块并从生成的模块中获取callable
对象,然后调用它。该值可能是一个不带点的字符串,在这种情况下它表示一个内置的可调用对象,例如PYTHONBREAKPOINT=int
。(Guido 表示偏爱正常的 Python 点路径,而不是 setuptools 风格的入口点语法 [语法]。)
此环境变量允许外部进程控制断点处理方式。一些用例包括:
- 完全禁用所有意外推送到生产环境的
breakpoint()
调用。这可以通过在执行环境中设置PYTHONBREAKPOINT=0
来实现。PEP 审阅者提出的另一个建议是在这种情况下设置PYTHONBREAKPOINT=sys.exit
。 - IDE 集成,为嵌入式执行提供专用调试器。IDE 会在其调试环境中运行程序,并将
PYTHONBREAKPOINT
设置为它们的内部调试钩子。
每次到达 sys.breakpointhook()
时,PYTHONBREAKPOINT
都会被重新解释。这允许进程在程序执行期间更改其值,并使 breakpoint()
响应这些更改。由于进入调试器本身就意味着停止执行,因此不认为这是一个性能关键部分。因此,程序可以执行以下操作:
os.environ['PYTHONBREAKPOINT'] = 'foo.bar.baz'
breakpoint() # Imports foo.bar and calls foo.bar.baz()
覆盖 sys.breakpointhook
会阻止默认的 PYTHONBREAKPOINT
咨询。如果覆盖代码想要咨询 PYTHONBREAKPOINT
,则由其自行决定。
如果访问 PYTHONBREAKPOINT
可调用对象以任何方式失败(例如,导入失败,或生成的模块不包含可调用对象),则会发出 RuntimeWarning
,并且不会调用断点函数。
请注意,与所有其他 PYTHON*
环境变量一样,当解释器以 -E
启动时,PYTHONBREAKPOINT
将被忽略。这意味着将发生默认行为(即 pdb.set_trace()
将运行)。曾有关于在 -E
存在时将 PYTHONBREAKPOINT=0
视为有效的一些讨论,但意见不一,因此决定这不足以构成一个特殊情况。
实施
存在一个包含建议实现的 pull request [实现]。
虽然实际实现是用 C 编写的,但此功能的 Python 伪代码大致如下:
# In builtins.
def breakpoint(*args, **kws):
import sys
missing = object()
hook = getattr(sys, 'breakpointhook', missing)
if hook is missing:
raise RuntimeError('lost sys.breakpointhook')
return hook(*args, **kws)
# In sys.
def breakpointhook(*args, **kws):
import importlib, os, warnings
hookname = os.getenv('PYTHONBREAKPOINT')
if hookname is None or len(hookname) == 0:
hookname = 'pdb.set_trace'
elif hookname == '0':
return None
modname, dot, funcname = hookname.rpartition('.')
if dot == '':
modname = 'builtins'
try:
module = importlib.import_module(modname)
hook = getattr(module, funcname)
except:
warnings.warn(
'Ignoring unimportable $PYTHONBREAKPOINT: {}'.format(
hookname),
RuntimeWarning)
return None
return hook(*args, **kws)
__breakpointhook__ = breakpointhook
被拒绝的替代方案
新关键词
最初,作者考虑了一个新关键字,或对现有关键字(如 break here
)进行扩展。这在几个方面被拒绝。
- 一个全新的关键字需要一个
__future__
来启用它,因为几乎任何新关键字都可能与现有代码冲突。这抵消了进入调试器的便利性。 - 像
break here
这样的扩展关键字,虽然更具可读性并且不需要__future__
,但它会将关键字扩展与这个新功能绑定,从而阻止更实用的扩展,例如 PEP 548 中提出的那些。 - 一个新的关键字需要修改语法,并且很可能需要一个新的字节码。这些都使得实现更加复杂。一个新的内置函数不会破坏任何现有代码(因为任何现有的模块全局变量只会遮蔽内置函数),并且非常容易实现。
sys.breakpoint()
为什么不是 sys.breakpoint()
?明确拒绝要求导入才能调用调试器,因为 sys
并未在每个模块中导入。那只会增加输入量,并导致:
import sys; sys.breakpoint()
它继承了本 PEP 旨在解决的几个问题。
版本历史
- 2019-10-13
- 在伪代码的
except
子句中添加缺失的return None
。
- 在伪代码的
- 2017-09-13
PYTHONBREAKPOINT
环境变量成为一等功能。
- 2017-09-07
debug()
重命名为breakpoint()
- 签名改为
breakpoint(*args, **kws)
,并直接传递给sys.breakpointhook()
。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0553.rst
最后修改时间:2025-02-01 08:55:40 GMT