本文同步发布于字节话云公众号。
前言
最近调试一段复杂代码的时候遇到一个问题,我在某处打了断点,并认为按照预期应该会运行到指定的断点,但遗憾的是并没有。几经排查,发现了一处隐藏的“坑”。
用简单的代码复现
简单起见,用下面这段代码来复现遇到的问题:
class Person(object):
def __init__(self, id):
self.id = id
self._person = None
self._name = None
@property
def name(self):
if self._person is None:
self._person = db_get_person(self.id)
return self._person['name']
def __repr__(self):
return '<Person: {}>'.format(self.name)
def db_get_person(id):
return {
'name': 'Jack'
}
if __name__ == '__main__':
p = Person('Jack')
print(p.name)
这段代码定义了一个 Person
类型,构造函数接收 id
参数。它有一个 name
属性,在首次访问时会调用 db_get_person()
(示例就直接返回了)函数从数据库根据 id
加载这个人的信息,并将之缓存,后续再访问时就返回缓存值。另外,还定义了 __repr__
魔法方法,为了在打印 Person
对象时能够更加友好。
让我们在第 18
、24
行打上断点并开始调试,首先会运行到第 24
行。当点击继续运行下一个断点时,会发现程序直接运行结束了,而没有如预期进行。
为什么 PyCharm 没有运行到指定断点?
仔细看下运行到第
24
行的调试界面,在 Variables 面板中会自动显示出当前作用域的所有变量。其中就有变量 p
,而它显示为了 <Person: Jack>
,这说明此时 PyCharm 已经自动运行了 Person.__repr__
,而此魔法方法调用了 self.name
。当运行第 24 行的 p.name
时,已不是首次运行。
看到这里就真相大白了,PyCharm 的 Variables 面板中的变量会被自动运行 Person.__repr__
。在此阶段中,该方法涉及到的逻辑如果有断点是不会被暂停的,换句话说也就不会运行到指定的断点。
PyCharm 调试界面中除了 Variables 面板可以查看变量外,Watches 面板也能够执行任意表达式来观察。如果使用不慎,也可能遇到相同问题。比如:断点运行到第 24 行时,在 Watches 面板中添加变量 p.name
该如何解决?
既然明白了原因,那解决的思路就是不让 PyCharm 自动运行到特定的代码。
方法一:取消第 24 行断点
由于取消了第 24 行断点,调试逻辑不会在第 24 行停留,那么 Variables 面板就不会显示变量 p
,也就不会调用 Person.__repr__
。在运行第 24 行的 p.name
时就是首次运行,自然就能运行到第 18 行断点。
方法二:临时注释掉 Person.repr
由于注释了 Person.__repr__
,调试停留在第 24 行时,尽管 Variables 面板显示了变量 p
,并不会通过 Person.__repr__
调用 p.name
。这样,也能够确保运行第 24 行的 p.name
是首次运行,自然就能运行到第 18 行断点。
总结
使用 PyCharm 调试具有缓存逻辑的代码时,要注意 Variables 和 Watches 面板中的变量是否已被自动执行对应的逻辑,从而导致没有进入到预期的断点。