问题
I'm re-implementing __getattribute__
for a class.
I want to notice any incorrect (meaning failures are expected, of course) failures of providing attributes (because the __getattribute__
implementation turned out quite complex). For that I log a warning if my code was unable to find/provide the attribute just before raising an AttributeError
.
I'm aware:
__getattribute__
implementations are encouraged to be as small as simple as possible.- It is considered wrong for a
__getattribute__
implementation to behave differently based on how/why it was called. - Code accessing the attribute can just as well
try/except
instead of usinghasattr
.
TL;DR: Nevertheless, I'd like to detect whether a call to __getattribute__
was done due to hasattr
(verses a "genuine" attempt at accessing the attribute).
回答1:
This is not possible, even through stack inspection. hasattr
produces no frame object in the Python call stack, as it is written in C, and trying to inspect the last Python frame to guess whether it's suspended in the middle of a hasattr
call is prone to all kinds of false negatives and false positives.
If you're absolutely determined to make your best shot at it anyway, the most reliable (but still fragile) kludge I can think of is to monkey-patch builtins.hasattr
with a Python function that does produce a Python stack frame:
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__
Calling probably_called_from_hasattr
inside __getattribute__
will then test if your __getattribute__
was probably called from hasattr
. This avoids any need to assume that the calling code used the name "hasattr", or that use of the name "hasattr" corresponds to this particular __getattribute__
call, or that the hasattr
call originated inside Python-level code instead of C.
The primary sources of fragility here are if someone saved a reference to the real hasattr
before the monkey-patch went through, or if someone else monkey-patches hasattr
(such as if someone copy-pastes this code into another file in the same program). The isinstance
check attempts to catch most cases of someone else monkey-patching hasattr
before us, but it's not perfect.
Additionally, if hasattr
on an object written in C triggers attribute access on your object, that will look like your __getattribute__
was called from hasattr
. This is the most likely way to get false positives; everything in the previous paragraph would give false negatives. You can protect against that by checking that the entry for obj
in the hasattr
frame's f_locals
is the object it should be.
Finally, if your __getattribute__
was called from a decorator-created wrapper, subclass __getattribute__
, or something similar, that will not count as a call from hasattr
, even if the wrapper or override was called from hasattr
, even if you want it to count.
回答2:
You can use sys._getframe
to get the caller frame and use inspect.getframeinfo
to get the line of code that makes the call, and then use some sort of parsing mechanism such as regex (you can't use ast.parse
since the one line of code is often an incomplete statement) to see if hasattr
is the caller. It isn't very robust but it should work in most reasonable cases:
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')
This outputs:
called by hasattr
called by something else
来源:https://stackoverflow.com/questions/52940409/detect-if-a-getattribute-call-was-due-to-hasattr