检测 __getattribute__ 调用是否由 hasattr 引起

Detect if a __getattribute__ call was due to hasattr

我正在为 class.

重新实现 __getattribute__

我想注意提供属性的任何不正确(当然意味着失败是预期的)失败(因为 __getattribute__ 实现非常复杂)。为此,如果我的代码在引发 AttributeError.

之前无法 find/provide 属性,我会记录一条警告

我知道:

  1. __getattribute__ 鼓励实施尽可能简单。
  2. __getattribute__ 实现根据调用的 how/why 表现不同被认为是错误的。
  3. 访问属性的代码也可以 try/except 而不是使用 hasattr

TL;DR:尽管如此,我想检测是否由于 hasattr 而调用 __getattribute__(节 "genuine" 尝试访问该属性)。

您可以使用 sys._getframe 获取调用者框架并使用 inspect.getframeinfo 获取进行调用的代码行,然后使用某种解析机制,例如正则表达式(您可以不要使用 ast.parse 因为一行代码通常是一个不完整的语句)来查看 hasattr 是否是调用者。它不是很健壮,但它应该在大多数合理的情况下工作:

import inspect
import sys
import re
class A:
    def __getattribute__(self, item):
        if re.search(r'\bhasattr\b', inspect.getframeinfo(sys._getframe(1)).code_context[0]):
            print('called by hasattr')
        else:
            print('called by something else')
hasattr(A(), 'foo')
getattr(A(), 'foo')

这输出:

called by hasattr
called by something else

这是不可能的,即使通过堆栈检查也是如此。 hasattr 在 Python 调用堆栈中不生成任何帧对象,因为它是用 C 编写的,并试图检查最后一个 Python 帧以猜测它是否在 [= 中间挂起11=] 调用容易出现各种误报和误报。

如果你下定决心无论如何都要尽力而为,我能想到的最可靠(但仍然脆弱)的拼凑就是使用 Python 猴子补丁 builtins.hasattr 产生 Python 栈帧的函数:

import builtins
import inspect
import types

_builtin_hasattr = builtins.hasattr
if not isinstance(_builtin_hasattr, types.BuiltinFunctionType):
    raise Exception('hasattr already patched by someone else!')

def hasattr(obj, name):
    return _builtin_hasattr(obj, name)

builtins.hasattr = hasattr

def probably_called_from_hasattr():
    # Caller's caller's frame.
    frame = inspect.currentframe().f_back.f_back
    return frame.f_code is hasattr.__code__

__getattribute__ 中调用 probably_called_from_hasattr 将测试您的 __getattribute__ 是否可能是从 hasattr 调用的。这避免了假设调用代码使用名称 "hasattr",或者名称 "hasattr" 的使用对应于这个特定的 __getattribute__ 调用,或者 hasattr 调用起源于 Python 级代码而不是 C.

这里脆弱性的主要来源是,如果有人在猴子补丁通过之前保存了对真实 hasattr 的引用,或者如果其他人猴子补丁 hasattr(例如有人将此代码复制粘贴到同一程序中的另一个文件中)。 isinstance 检查试图在我们之前捕获大多数其他人进行猴子修补 hasattr 的情况,但它并不完美。

此外,如果用 C 编写的对象上的 hasattr 触发了对您的对象的属性访问,那看起来您的 __getattribute__ 是从 hasattr 调用的。这是最有可能得到误报的方法;上一段中的所有内容都会给出假阴性。您可以通过检查 hasattr 框架的 f_localsobj 的条目是否是它应该是的对象来防止这种情况发生。

最后,如果你的 __getattribute__ 是从装饰器创建的包装器、子类 __getattribute__ 或类似的东西调用的,那将不会算作来自 hasattr 的调用,即使包装器或覆盖是从 hasattr 调用的,即使您希望它计数。