0

I have a metaclass and and class that both use __getattribute__ to intercept attribute calls. They look like this:

class B(type):    
    def __getattribute__ (self, name) :
        print(f'hello {self}')
        return super().__getattribute__(name)

class C(metaclass=B):
    MY_ATTR = 'hello attr'

    def __getattribute__ (self, name) :
        print(f'hello {self}')
        return super().__getattribute__(name)

This behaves as I intend:

C.MY_ATTR  
# hello <class 'C'>
# 'hello attr'

C().MY_ATTR
# hello <C object at 0x10bcbbda0>
# 'hello attr'

Now I want to take that duplicate code from B and C and let it be inherited. Lucky I called them B and C and left room for A. Here we go:

class A:
    def __getattribute__ (self, name) :
        print(f'hello {self}')
        return super().__getattribute__(name)

class B(type, A):
    pass

class C(A, metaclass=B):
    MY_ATTR = 'hello attr'

Unfortunately, this no longer behaves as before:

C.MY_ATTR  
# 'hello attr'

C().MY_ATTR
# hello <C object at 0x10bcbbda0>
# 'hello attr'

I assume the problem is something around a MetaClass not being able to inherit from a regular class but I'm unsure. I'm also open to any other implementations (maybe not requiring a metaclass) of getting the same behaviour - though I also still want calls like C.MISSING to raise a AttributeError.

There are similar questions to this (e.g. Get attributes for class and instance in python) but they are slightly different and don't achieve what I'm trying to.

Thanks

2
  • OOC, what happens if you inherit from A first, e.g. class B(A, type):? Commented Mar 2, 2017 at 2:18
  • Yeah, I had tried this. C.MY_ATTR raises an exception. hello <class 'C'> __getattribute__ TypeError: expected 1 arguments, got 0 Commented Mar 2, 2017 at 3:41

1 Answer 1

0

It is a bit more complicated than that - Metaclasses do accept ordinary classes as mixins - and any methods provided on these mixins will be available on the classes using those metaclasses as if they were classmethods (but they will be hidden from the instances - as attribute lookup on a n instance looks for the attribute on the class, but not on the metaclass).

However __getattribute__ is a beast - it is hard to do it correctly even if no metaclass are involved (unlike __getattr__).

I did a bit of trial and error myself, and got that type's __getattribute__ does exist and is different from object's - that is why you custom version is not called when you do the class M(type, A) inheritance. And also, if you swap the inheritance order, you see that super() gets a corner case where it does not fill in the needed parameters to properly call type.__getattribute__ .

I've tried to hardcode the call to type.__getattribute__ with an if statement, and got in an attribute lookup loop, I could not figure out - the thing is non-trivial (but probably due to IPython introspection - see bellow).

Nonetheless, reusing code on the class and metaclass itself is something I had never seem.

If what you want to do in the __getattribute__ body is nontrivial (as opposed to the example's print) I suggest you just factor the __getattribute__body out to an ordinary function, and call it explicitly where you put the print on your examples - and just keep the three line __getattribute__ on both the metaclass and class base:

def getattribute(instance_or_class, attr_name):
    # complex attr_name consuming body goes here.
    # even colateral effects on "self" can be applied
    # to the  "instance_or_class" object
    print(instance_or_class, attr_name)


class B(type):
    def __getattribute__ (cls, name):
        getattribute(cls, name)
        return super().__getattribute__(name)


class C(metaclass=B):
    MY_ATTR = 'hello attr'
    def __getattribute__ (self, name):
        getattribute(self, name)
        return super().__getattribute__(name)

Now, one thing I see different from your first output - I actually get:

In [43]: C().MY_ATTR
<class '__main__.C'> __class__
<class '__main__.C'> __class__
<__main__.C object at 0x7fa420a314a8> MY_ATTR
Out[43]: 'hello attr'

That is, Python first looks up C __class__ attribute -

Wait, on double checking, when doing it on the regular Python prompt, these calls do not occur - it is Ipython that triggers then, while at it's introspection magic things. It maybe the recursion loop I fell into was due to IPython's introspections - nonetheless, you will be avoiding a lot of unwanted magic letting the __getattribute__ explicit in the metaclass and base classes, and factoring it out as above.

>>> C().MY_ATTR
<__main__.C object at 0x7f0d4d908898> MY_ATTR
'hello attr'
>>> C.MY_ATTR
<class '__main__.C'> MY_ATTR
'hello attr'
>>> 
Sign up to request clarification or add additional context in comments.

1 Comment

Great response, thank you. I actually did try separating out the logic into an external function but that caused its own set of errors. This was a while back now so I can't remember specifically. Though, I may have been using the REPL and was getting inspection errors - like you mentioned. I'll see if I have that code committed in git or if I can reproduce. Thanks again

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.