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 [linters])会对此行发出警告,因为它包含两个语句。将习惯用法分解成两行会使其使用变得复杂,因为在清理时有更多出错的机会。即,当您不再需要调试代码时,您可能会忘记删除这些行之一。
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__
的工作方式完全相同 [hooks]。
内置函数的签名为 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 风格的入口点语法 [syntax]。)
此环境变量允许外部进程控制如何处理断点。一些用例包括
- 完全禁用推送到生产环境的所有意外
breakpoint()
调用。这可以通过在执行环境中设置PYTHONBREAKPOINT=0
来实现。PEP 审阅者提出的另一个建议是在这种情况下设置PYTHONBREAKPOINT=sys.exit
。 - 与嵌入式执行的专用调试器的 IDE 集成。IDE 将在其调试环境中运行程序,并将
PYTHONBREAKPOINT
设置为其内部调试挂钩。
PYTHONBREAKPOINT
在每次到达 sys.breakpointhook()
时都会重新解释。这允许进程在程序执行期间更改其值,并使 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
作为有效处理,但意见不一致,因此决定这不够特殊,不值得作为特殊情况处理。
实现
已存在包含提议实现的拉取请求 [impl]。
虽然实际实现是在 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