Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

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 [代码检查工具])会抱怨这一行,因为它包含两个语句。将这种惯用法分解成两行会使其使用复杂化,因为在清理时更容易出错。也就是说,当你不再需要调试代码时,你可能会忘记删除其中一行。

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