0

Are there any functions like the built-in functions getattr and hasattr in the standard library but which bypass instance attributes during attribute lookup, like the implicit lookup of special methods?

Let’s call these hypothetical functions getclassattr and hasclassattr. Here are the implementations that I would expect:

null = object()

def getclassattr(obj, name, default=null, /):
    if not isinstance(name, str):
        raise TypeError('getclassattr(): attribute name must be string')
    try:
        classmro = vars(type)['__mro__'].__get__(type(obj))
        for cls in classmro:
            classdict = vars(type)['__dict__'].__get__(cls)
            if name in classdict:
                attr = classdict[name]
                attrclassmro = vars(type)['__mro__'].__get__(type(attr))
                for attrclass in attrclassmro:
                    attrclassdict = vars(type)['__dict__'].__get__(attrclass)
                    if '__get__' in attrclassdict:
                        return attrclassdict['__get__'](attr, obj, type(obj))
                return attr
        classname = vars(type)['__name__'].__get__(type(obj))
        raise AttributeError(f'{classname!r} object has no attribute {name!r}')
    except AttributeError as exc:
        try:
            classmro = vars(type)['__mro__'].__get__(type(obj))
            for cls in classmro:
                classdict = vars(type)['__dict__'].__get__(cls)
                if '__getattr__' in classdict:
                    return classdict['__getattr__'](obj, name)
        except AttributeError as exc_2:
            exc = exc_2
        except BaseException as exc_2:
            raise exc_2 from None
        if default is not null:
            return default
        raise exc from None
def hasclassattr(obj, name, /):
    try:
        getclassattr(obj, name)
    except AttributeError:
        return False
    return True

A use case is a pure Python implementation of the built-in class classmethod:*

import types

class ClassMethod:

    def __init__(self, function):
        self.__func__ = function

    def __get__(self, instance, owner=None):
        if instance is None and owner is None:
            raise TypeError('__get__(None, None) is invalid')
        if owner is None:
            owner = type(instance)
        # Note that we use hasclassattr instead of hasattr here.
        if hasclassattr(self.__func__, '__get__'):
            # Note that we use getclassattr instead of getattr here.
            return getclassattr(self.__func__, '__get__')(owner, type(owner))
        return types.MethodType(self.__func__, owner)

    @property
    def __isabstractmethod__(self):
        return hasattr(self.__func__, '__isabstractmethod__')

* Note that this implementation would not work with the built-in functions getattr and hasattr since they look up in instance attributes first, as this comparison with the built-in class classmethod shows:

>>> import types
>>> class ClassMethod:
...     def __init__(self, function):
...         self.__func__ = function
...     def __get__(self, instance, owner=None):
...         if instance is None and owner is None:
...             raise TypeError('__get__(None, None) is invalid')
...         if owner is None:
...             owner = type(instance)
...         if hasattr(self.__func__, '__get__'):
...             return getattr(self.__func__, '__get__')(owner, type(owner))
...         return types.MethodType(self.__func__, owner)
...     @property
...     def __isabstractmethod__(self):
...         return hasattr(self.__func__, '__isabstractmethod__')
... 
>>> class M(type):
...     def __get__(self, instance, owner=None):
...         return 'metaclass'
... 
>>> class A(metaclass=M):
...     def __get__(self, instance, owner=None):
...         return 'class'
... 
>>> ClassMethod(A).__get__('foo')
'class'
>>> classmethod(A).__get__('foo')
'metaclass'
Maggyero
  • 4,569
  • 3
  • 30
  • 49
  • Why not just... use `getattr` or `hasattr` on the class, instead of on the instance? – Karl Knechtel Apr 14 '21 at 22:43
  • 1
    @KarlKnechtel: That causes problems with metaclasses and descriptors. – user2357112 Apr 14 '21 at 22:44
  • @KarlKnechtel Because this would also look up the attribute on the metaclass, so `ClassMethod(a).__get__(3)` would return `'metaclass'` (not `'class'`) in the last code snippet. – Maggyero Apr 14 '21 at 22:46
  • I'm afraid I still don't understand the problem. Could you show an example of the `getclassattr` implementation *directly* doing something different from the built-in `getattr` applied to the class? – Karl Knechtel Apr 14 '21 at 23:32
  • @KarlKnechtel Yes: `class A: x = 'foo'; a = A(); a.x = 'bar'; print(getattr(a, 'x')); print(getclassattr(a, 'x'))` prints `bar\nfoo`. – Maggyero Apr 15 '21 at 13:44
  • No, I said to apply `getattr` to the *class*. Like `getattr(a.__class__, 'x')`. – Karl Knechtel Apr 15 '21 at 15:28
  • @KarlKnechtel Sorry I misunderstood: `class M(type): x = 'bar'`, `class A(metaclass=M): x = 'foo'`, `print(getattr(A, 'x'))`, `print(getclassattr(A, 'x'))` prints `foo\nbar`. – Maggyero Apr 15 '21 at 15:35
  • Ah, so you don't only want to "skip the instance dictionary" but also explicitly *prioritize* the metaclass dictionary, which normally doesn't happen for *either* an instance or class lookup (although, it is consulted for class lookups if not otherwise found)... ? – Karl Knechtel Apr 15 '21 at 15:41
  • @KarlKnechtel A class is a metaclass instance so there is no special metaclass handling here, we just skip the (metaclass) instance dictionary like usual. – Maggyero Apr 15 '21 at 15:49
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/231178/discussion-between-karl-knechtel-and-maggyero). – Karl Knechtel Apr 15 '21 at 15:49
  • @user2357112supportsMonica Do you have any idea of what ‘advanced usages’ Guido is referring to [here](https://stackoverflow.com/a/67425120/2326961)? – Maggyero May 06 '21 at 21:05

1 Answers1

0

Instead of introducing the new functions getclassattr and hasclassattr to bypass instance attributes during attribute lookup, like the implicit lookup of special methods, an alternative approach is to introduce a proxy class (let’s call it bypass) that overrides the method __getattribute__. I think this may be a better approach since the method __getattribute__ is a hook designed for customising attribute lookup, and it works with the built-in functions getattr and hasattr but also with the attribute retrieval operator .:

class bypass:

    def __init__(self, subject):
        self.subject = subject

    def __getattribute__(self, name):
        obj = super().__getattribute__('subject')
        classmro = vars(type)['__mro__'].__get__(type(obj))
        for cls in classmro:
            classdict = vars(type)['__dict__'].__get__(cls)
            if name in classdict:
                attr = classdict[name]
                attrclassmro = vars(type)['__mro__'].__get__(type(attr))
                for attrclass in attrclassmro:
                    attrclassdict = vars(type)['__dict__'].__get__(attrclass)
                    if '__get__' in attrclassdict:
                        return attrclassdict['__get__'](attr, obj, type(obj))
                return attr
        classname = vars(type)['__name__'].__get__(type(obj))
        raise AttributeError(f'{classname!r} object has no attribute {name!r}')

class M(type):
    x = 'metaclass'

class A(metaclass=M):
    x = 'class'

a = A()
a.x = 'object'

assert getattr(a, 'x') == 'object' and getattr(bypass(a), 'x') == 'class'
assert getattr(A, 'x') == 'class' and getattr(bypass(A), 'x') == 'metaclass'
Maggyero
  • 4,569
  • 3
  • 30
  • 49