Search code examples
pythonattributesidiomsstandard-libraryclass-variables

Are there any functions like getattr/hasattr but which skip instance attributes?


Are there any functions like the built-in functions getattr and hasattr in the standard library but which skip 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 in which the hypothetical functions getclassattr and hasclassattr look up the attribute '__get__' on the class type(self.__func__):*

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 here, not hasattr.
        if hasclassattr(self.__func__, '__get__'):
            # Note that we use getclassattr here, not getattr.
            return getclassattr(self.__func__, '__get__')(owner, type(owner))
        return types.MethodType(self.__func__, owner)

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

class M(type):
    pass

class C(metaclass=M):
    def __get__(self, instance, owner=None):
        pass

assert ClassMethod(C).__get__(123) == classmethod(C).__get__(123)

* Note that calling the hypothetical functions getclassattr and hasclassattr on self.__func__ instead of calling the built-in functions getattr and hasattr on it does not work since they look up the attribute '__get__' both on the instance self.__func__ and on the class type(self.__func__):

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):
    pass

class C(metaclass=M):
    def __get__(self, instance, owner=None):
        pass

assert ClassMethod(C).__get__(123) != classmethod(C).__get__(123)

Calling the built-in functions getattr and hasattr on type(self.__func__) does not work either since they look up the attribute '__get__' both on the instance type(self.__func__) and on the class type(type(self.__func__)):

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(type(self.__func__), '__get__'):
            return getattr(type(self.__func__), '__get__')(owner, type(owner))
        return types.MethodType(self.__func__, owner)
    @property
    def __isabstractmethod__(self):
        return hasattr(self.__func__, '__isabstractmethod__')

class MM(type):
    def __get__(self, instance, owner=None):
        pass

class M(type, metaclass=MM):
    pass

class C(metaclass=M):
    pass

assert ClassMethod(C).__get__(123) != classmethod(C).__get__(123)

Solution

  • Instead of introducing new functions getclassattr and hasclassattr to skip 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 skip) that overrides the method __getattribute__. I think it is 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 skip:
        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'
    assert getattr(skip(a), 'x') == 'class'
    assert getattr(A, 'x') == 'class'
    assert getattr(skip(A), 'x') == 'metaclass'