breakpoint() <Python 内置函数>

转载须注明出处:简书@Orca_J35 | GitHub@orca-j35

breakpoint() 是 Python 3.7 中新增加的内置函数,本文介绍了该函数的使用方法,目录结构如下:

目录.jpg

1. how2 Implementation

虽然 breakpoint 是由 C 语言实现的 ,但我们可以使用 Python pseudo-code 来描述其实现过程:

# 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 hook(*args, **kws)

__breakpointhook__ = breakpointhook

2. how2 work

在未设置 PYTHONBREAKPOINT 的情况下,breakpoint() 会中断当前程序并进入 pdb 调试器。具体工作方式请查看相应子章节。

2.1 breakpoint()

breakpoint(*args, **kws)

伪代码:

# 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)

可见 breakpoint 的工作仅是设置并调用 hook 函数,这里只需要注意以下几点:

  • breakpoint 中的 hook 变量将引用 sys.breakpointhook 函数对象;
  • breakpoint 会将自己所有的实参都传递给 sys.breakpointhook()
  • 如果 sys.breakpointhook 缺失,则会抛出 RuntimeError

2.2 breakpointhook()

sys.breakpointhook(*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:
        # 模块导入失败,或funcname不可调用都会引发RuntimeWarning
        module = importlib.import_module(modname)
        hook = getattr(module, funcname)
    except:
        # 如果抛出异常,则不会执行hook(*args, **kws)
        warnings.warn(
            'Ignoring unimportable $PYTHONBREAKPOINT: {}'.format(
                hookname),
            RuntimeWarning)
    # 如果实参与函数签名中的参数不匹配,则会抛出TypeError
    return hook(*args, **kws) 

__breakpointhook__ = breakpointhook

sys.breakpointhook 作为 breakpoint 的钩子函数,是正真实现具体功能的地方。

sys.breakpointhook 会访问环境变量 PYTHONBREAKPOINT,从而确定 hook 的引用对象,也就是说 PYTHONBREAKPOINT 的状态对执行结果有决定性的作用。

具体而言,PYTHONBREAKPOINT 存在如下几种状态:

  • 完全不设置该环境变量:此时,hook 会引用 pdb.set_trace,所以最终会进入 pdb 调试器。(提示,由于 pdb.set_trace(*, header=None) 只接受关键字参数 header,因此不要向 breakpoint() 传递任何其它参数)
  • PYTHONBREAKPOINT=:此时,环境变量的值为空字符串,这与完全不设置该环境变量的效果相同。
  • PYTHONBREAKPOINT=0:此时,breakpointhook 会立即返回 None,从而禁用调试;
  • PYTHONBREAKPOINT=some.importable.callable:此时,breakpointhook() 将导入 some.importable 模块,然后通过 hook 引用模块中的 callable 对象
  • PYTHONBREAKPOINT=callable:此时,callable 表示一个内置可调用对象,如 PYTHONBREAKPOINT=print

每次调用 sys.breakpointhook() 时,都会访问 PYTHONBREAKPOINT 变量。如果在程序执行期间改变了 PYTHONBREAKPOINT 的值, sys.breakpointhook() 便会读取变化后的值。因此,程序可以执行如下操作:

os.environ['PYTHONBREAKPOINT'] = 'foo.bar.baz'
breakpoint()    # Imports foo.bar and calls foo.bar.baz()

注意:如果在解释器启动时伴随参数 -E,便会忽略所有 PYTHON* 环境变量(PYTHONBREAKPOINT 也不列外)。这意味着breakpoint() 会遵守默认行为,即中断当前程序并进入 pdb 调试器。

2.3 __breakpointhook__

# In sys.
__breakpointhook__ = breakpointhook

在初始化时 ,__breakpointhook__breakpointhook 会引用相同的函数对象。因此,就算重写了 breakpointhook 函数,也可通过 __breakpointhook__ 将其重置:

# 重置 breakpointhook 
sys.breakpointhook = sys.__breakpointhook__

sys.breakpointhook / sys.__breakpointhook__ 工作方式与 sys.displayhook() / sys.__displayhook__sys.excepthook() / sys.__excepthook__ 完全相同。

3. with pdb.set_trace

在使用 pdb 调试器时,我们通常会这样设置断点:

def divide(e, f):
    # 设置一个断点,执行至此处后,便会中断当前程序并进入pdb调试器。
    import pdb; pdb.set_trace()
    return f / e

在 Python 3.7 中,我们可以通过 breakpoint() 函数来设置断点(前提是未定义 PYTHONBREAKPOINT环境变量,或该环境变量的值等于空字符串)。

"""file_name:bugs.py"""
def divide(e, f):
    breakpoint(header="进入调试器")
    return e / f

a, b = 1, 9
print(divide(a, b))

此时,breakpointhook() 会返回 pdb.set_trace() ,从而进入 pdb 调试器。

Tips:在 Python 3.7 中,pdb.set_trace 仅支持关键字参数 header,不接受其他任何参数。如果传递了错误的参数,便会引发 TypeError

bugs.py 的执行效果如下:

$ python.exe bugs.py
进入调试器
> c:\users\iwhal\desktop\pytest\bugs.py(3)divide()
-> return e / f
(Pdb) p a+b
10
(Pdb) c
0.1111111111111111

Tips:命令 p 用于查看表达式的值;命令 c 用于退出调试器,并继续执行程序。

4. with pudb.set_trace

通过 PYTHONBREAKPOINT 还可选用别的调试器。例如,当我们想要使用 PuDB (一个基于控制台的可视化调试器)时,只需修改 PYTHONBREAKPOINT

$ PYTHONBREAKPOINT=pudb.set_trace python3.7 bugs.py

注意:这里需要手动安装 pudb (pip install pudb),并且 pudb 仅支持类 UNIX 环境。由于我是 windows 环境,所以无法进行更详细的演示。

总之,通过修改PYTHONBREAKPOINT 的值,我们可以选择自己需要的调试器。但是同样需要注意的是:通过 breakpoint 传递的参数,是否与调试器的函数签名匹配。

pudb.png

5. with web_pdb.set_trace

如果想要使用 Web-PDB (Web-PDB 在内置 PDB 上增加了 web 界面,并允许在 web 浏览器中远程调试 Python 脚本),同样只需修改 PYTHONBREAKPOINT。注意:这里需要手动安装 web-pdb (pip install web-pdb)。

为了方便演示,先创建一个小脚本:

"""file_name:test.py"""
a, b = 1, 2
breakpoint()
a, b = b, a

修改 PYTHONBREAKPOINT ,并执行脚本:

$ PYTHONBREAKPOINT=web_pdb.set_trace python.exe test.py
2018-08-23 10:54:05,108: root - web_console:110 - CRITICAL - Web-PDB: starting web-server on LAPTOP-AR0R702R:5555...

之后便可通过浏览器,在 5555 端口上进入 Web-PDB 调试器:

web-pdb.jpg

6. with IPython.embed

breakpoint 函数不仅适用于调试器,也适用于任何可调用对象,只要参数匹配即可。因此,当我们想要在程序执行过程中,启动一个交互式 shell 时(比如 IPython),同样只需修改 PYTHONBREAKPOINT,如下:

$ PYTHONBREAKPOINT=IPython.embed python3.7 bugs.py
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help.

进入调试器

In [1]: a,b
Out[1]: (1, 9)

In [2]: quit()

0.1111111111111111

6.1 what's IPython.embed

ipython-embed

IPython.embed(**kwargs) 会在程序的当前运行位置嵌入 IPython。在第一次调用该方法时,会先创建 InteractiveShellEmbed 类的一个实例,并调用该实例。再次调用该方法时,会直接调用之前创建的 InteractiveShellEmbed 实例。默认情况下,InteractiveShellEmbed 实例和当前程序使用相同的命名空间。另外,IPython.embed 允许我们在嵌入点打印指定字符串。

"""file_name:test.py"""
from IPython import embed
a = 10
b = 20
embed(header='First time')# 会在嵌入点打印该字符串
c = 30
d = 40
embed()

执行 test.py 脚本后,会分两次进入 IPython:

$ C:/Python37/python.exe c:/Users/iwhal/Desktop/PyTest/test.py
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help.


First time

In [1]: a,b
Out[1]: (10, 20)

In [2]: quit()

Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: a,b,c,d
Out[1]: (10, 20, 30, 40)

In [2]: quit()

7. with Functions

breakpoint 函数不仅适用于调试器,也适用于任何可调用对象。因此,我们可以通过 breakpoint 调用自定义函数,只要参数匹配即可。。

如下代码将创建一个可打印本地作用域内所有变量的函数:

"""file_name:bp_utils.py"""
from pprint import pprint
import sys

def print_locals(header=None):
    print(header)
    caller = sys._getframe(1)  # Caller is 1 frame up.
    pprint(caller.f_locals)

只要合理设置 PYTHONBREAKPOINT 便可调用该函数:

$ PYTHONBREAKPOINT=bp_utils.print_locals python.exe bugs.py
First time
{'e': 1, 'f': 9}
0.1111111111111111

基于同样的思路,我们还可以调用内置函数,比如 print 函数。
先创建一个脚本,用于向内置传递传输,只需包含 breakpoint 即可:

"""file_name:built_in.py"""
breakpoint("hello")

执行该脚本:

$ PYTHONBREAKPOINT=print python3.7 built_in.py
hello

7.1 what's sys._getframe

sys._getframe([depth])

从调用栈(call stack)中返回一个帧(frame)对象。如果给出了 depth 参数(需是整数),则会返回栈顶下方对应深度的帧对象。如果 depth 值超过了调用栈的深度,将抛出 ValueError 异常。depth 的默认值是 0,此时会返回调用栈最顶部的帧。

CPython implementation detail: 该函数仅用于内部和专门用途。并不保证在所有 Python 实现中都存在。

8. 参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容