2

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:

  1. __getattribute__ implementations are encouraged to be as small as simple as possible.
  2. It is considered wrong for a __getattribute__ implementation to behave differently based on how/why it was called.
  3. Code accessing the attribute can just as well try/except instead of using hasattr.

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

NirIzr
  • 3,131
  • 2
  • 30
  • 49

2 Answers2

3

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.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Thanks! overwriting `hasattr` is indeed a simple solution. I should have thought of that myself! – NirIzr Oct 23 '18 at 04:11
-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
blhsing
  • 91,368
  • 6
  • 71
  • 106
  • This fails in plenty of reasonable cases, such as `if hasattr(thing, 'existing_attribute') and thing.existing_attribute == 5` or `if hasattr(different_thing, 'foo') or thing_with_getattribute.bar == 5`. – user2357112 Oct 23 '18 at 03:19
  • Thanks for your answer! I've consider doing something similar but parsing the line is prone to errors. I hope to find something that is more error resistant, or at least has less false positives (I.e. I can accept missing `hasattr`-based calls but rather not miss "genuine" calls). – NirIzr Oct 23 '18 at 03:24
  • @user2357112 Yes, but if one actually needs the value of an attribute then he/she shouldn't use `hasattr` in the first place, but instead use `getattr` with a default value, or use a `try` block around `thing.existing_attribute`, to avoid a redundant and potentially dangerous (when there are multiple threads) call to `__getattribute__`. One should only use `hasattr` if all he/she cares about is the existence of the attribute but not the value of the attribute. If the coder follows this principle then the flaw with my method would not be an issue. – blhsing Oct 23 '18 at 03:33
  • 1
    The very premise of this question is asking about how to do a thing that violates design principles. It seems unsafe to assume that the rest of the codebase stringently follows any particular principles. Also, `getattr` with a default is awkward when you need to be able to distinguish between attribute absence and an attribute value that happens to be the default, and this still fails even in cases where the rules you list are followed. – user2357112 Oct 23 '18 at 03:45