如何获取缺少属性的对象
假设我们试图访问一个不存在的属性:
>>> {'foo': 'bar'}.gte('foo') # well, I meant “get”!
在Python中,如果你遇到AttributeError
,它只会包含一个叫args
的属性,里面有一个字符串,内容是错误信息,比如:'dict' object has no attribute 'gte'
,意思是字典对象没有这个属性。
使用inspect
和/或traceback
模块配合sys.last_traceback
,有没有办法获取到实际的字典对象呢?
>>> offending_object = get_attributeerror_obj(sys.last_traceback)
>>> dir(offending_object)
[...
'clear',
'copy',
'fromkeys',
'get', # ah, here it is!
'items',
...]
补充:既然这个秘密已经被揭开,我就分享一下我的发现和代码(请不要解决这个问题并提交到PyPI哦;))
这个AttributeError
是在这里创建的,明显没有附带原始对象的引用。
下面是带有相同占位符函数的代码:
import sys
import re
import difflib
AE_MSG_RE = re.compile(r"'(\w+)' object has no attribute '(\w+)'")
def get_attributeerror_obj(tb):
???
old_hook = sys.excepthook
def did_you_mean_hook(type, exc, tb):
old_hook(type, exc, tb)
if type is AttributeError:
match = AE_MSG_RE.match(exc.args[0])
sook = match.group(2)
raising_obj = get_attributeerror_obj(tb)
matches = difflib.get_close_matches(sook, dir(raising_obj))
if matches:
print('\n\nDid you mean?', matches[0], file=sys.stderr)
sys.excepthook = did_you_mean_hook
2 个回答
我想分享一下我的经验,因为我成功地尝试过类似的事情,针对的是DidYouMean-Python。
这里的关键在于,这几乎是唯一一个错误信息包含足够信息来推测你真正想要的情况。实际上,重要的是你试图在一个dict
对象上调用gte
:你需要的是类型,而不是对象本身。
如果你写的是{'foo': 'bar'}.get('foob')
,那么处理起来就会复杂得多,我很想知道有没有人有解决方案。
第一步
检查你是否在处理一个AttributeError(使用钩子的第一个参数)。
第二步
从错误信息中提取相关信息(使用第二个参数)。我用正则表达式来做这个。请注意,这个异常可能会根据Python的版本、你调用方法的对象等情况有多种形式。
到目前为止,我的正则表达式是:"^'?(\w+)'? (?:object|instance) has no attribute '(\w+)'$"
第三步
获取与类型对应的类型对象(在你的情况下是'dict'),这样你就可以在它上面调用dir()
。一个简单的解决方案是直接使用eval(type)
,但你可以通过重用追踪信息(钩子的第三个参数)来做得更好、更干净:追踪的最后一个元素包含了异常发生时的帧,在那个帧中,类型是被正确定义的(可能是局部类型、全局类型或内置类型)。
一旦你得到了类型对象,你只需要在它上面调用dir()
,然后提取你最喜欢的建议。
如果你需要更多关于我所做的事情的细节,请告诉我。
这不是你想要的答案,但我很确定你不能这样做……至少不能通过 sys.excepthook
。这是因为在处理错误时,引用计数会减少,所以在调用 sys.excepthook
之前,对象可能已经被垃圾回收了。实际上,这在 CPython 中就是这样发生的:
import sys
class X:
def __del__(self):
print("deleting")
def error():
X().wrong
old_hook = sys.excepthook
def did_you_mean_hook(type, exc, tb):
print("Error!")
sys.excepthook = did_you_mean_hook
error()
#>>> deleting
#>>> Error!
不过,这并不总是如此。因为异常对象指向当前的执行框架,如果你的代码看起来像这样:
def error():
x = X()
x.wrong
x
还不能被回收。x
是由当前框架拥有的,而这个框架还在运行。但是,既然我已经证明没有对这个对象的明确引用,那么该怎么做就不太明显了。例如,
def error():
foo().wrong
可能有一个对象还存活着,也可能没有,而唯一可行的方法就是运行 foo
……但即便如此,你也会遇到副作用的问题。
所以不,这种情况是不可能的。如果你不介意采取任何极端措施,你可能最终需要在加载时重写抽象语法树(AST),这类似于 FuckIt.py。不过你可能不想这么做。
我的建议是尝试使用一个代码检查工具(linter),来获取所有已知类及其方法的名称。你可以利用这些信息来逆向工程错误追踪字符串,以找出类和错误的方法,然后进行模糊匹配来找到建议。