Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

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

上次修改:2023-09-09 17:39:29 GMT