Here's an exploration of some weird Python functionality: how do we intercept class attribute access?
The following won't work.
class A: x = 1 def __getattribute__(self, name): print('{}.__getattribute__({})'.format(self, name)) return super().__getattribute__(name) print('A.x =', A.x) # A.x = 1
What about setting __getattribute__
to be a classmethod
?
class A: x = 1 @classmethod def __getattribute__(cls, name): print('{}.__getattribute__({})'.format(cls, name)) return super().__getattribute__(name) print('A.x =', A.x) # A.x = 1
Nope! What about the metaclass
?
class Meta(type): def __getattribute__(cls, name): print('{}.__getattribute__({})'.format(cls, name)) return super().__getattribute__(name) class A(metaclass=Meta): x = 1 print('A.x =', A.x) # <class '__main__.A'>.__getattribute__(x) # A.x = 1
Finally!
Can we introduce additional methods to our class besides the usual ones?
class Meta(type): def __getattribute__(cls, name): print('{}.__getattribute__({})'.format(cls, name)) return super().__getattribute__(name) def foo(cls, x): print('{}.foo({})'.format(cls, x)) class A(metaclass=Meta): x = 1 @classmethod def bar(cls, y): print('{}.bar({})'.format(cls, y)) print('dir(Meta) =', dir(Meta)) # dir(Meta) = ['__abstractmethods__', '__base__', '__bases__', '__basicsize__', '__call__', '__class__', '__delattr__', '__dict__', '__dictoffset__', '__dir__', '__doc__', '__eq__', '__flags__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__instancecheck__', '__itemsize__', '__le__', '__lt__', '__module__', '__mro__', '__name__', '__ne__', '__new__', '__prepare__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasscheck__', '__subclasses__', '__subclasshook__', '__text_signature__', '__weakrefoffset__', 'foo', 'mro'] # <class '__main__.A'>.__getattribute__(__dict__) # <class '__main__.A'>.__getattribute__(__bases__) print('dir(A) =', dir(A)) # dir(A) = ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'x']
A.foo()
does not exists!
print('A.foo(7) =', A.foo(7)) # <class '__main__.A'>.__getattribute__(foo) # <class '__main__.A'>.foo(7) # A.foo(7) = None
But it does!? Furthermore, it behaves like a class method
import types print(isinstance(A.foo, types.ClassMethodDescriptorType)) # <class '__main__.A'>.__getattribute__(foo) # False # <class '__main__.A'>.__getattribute__(foo) print('type(A.foo) =', type(A.foo)) # type(A.foo) = <class 'method'> # <class '__main__.A'>.__getattribute__(bar) print('type(A.bar) =', type(A.bar)) # type(A.bar) = <class 'method'> # <class '__main__.A'>.__getattribute__(__dict__) # <class '__main__.A'>.__getattribute__(__bases__) print('dir(A) =', dir(A)) # dir(A) = ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'x']
What about data attributes on a metaclass
?
class Meta(type): x = 10 class A(metaclass=Meta): pass print('dir(Meta) =', dir(Meta)) # dir(Meta) = ['__abstractmethods__', '__base__', '__bases__', '__basicsize__', '__call__', '__class__', '__delattr__', '__dict__', '__dictoffset__', '__dir__', '__doc__', '__eq__', '__flags__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__instancecheck__', '__itemsize__', '__le__', '__lt__', '__module__', '__mro__', '__name__', '__ne__', '__new__', '__prepare__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasscheck__', '__subclasses__', '__subclasshook__', '__text_signature__', '__weakrefoffset__', 'mro', 'x'] print('type(Meta.x) =', type(Meta.x)) # type(Meta.x) = <class 'int'> print('Meta.x =', Meta.x) # Meta.x = 10 print('dir(A) =', dir(A)) # dir(A) = ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__'] print('A.x =', A.x) # A.x = 10
Does this appear on the instance?
a = A() print('a = A()') # a = A() try: print('a.x =', a.x) except AttributeError: print('a.x doesn\'t exist') # a.x doesn't exist
Nope!
Is this metaclass attribute shared between classes?
class B(metaclass=Meta): pass print('B.x =', B.x) # B.x = 10 print('Setting B.x = 19') # Setting B.x = 19 B.x = 19 print('A.x =', A.x) # A.x = 10 print('B.x =', B.x) # B.x = 19
Nope! A.x
is unchanged and now B.x
is a new value
print('dir(A) =', dir(A)) # dir(A) = ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__'] print('dir(B) =', dir(B)) # dir(B) = ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'x']
Because B.x
is now a proper class attribute unlike Meta.x
.
What happens if I set a metadescriptor?
from weakref import WeakKeyDictionary class MetaAttr: def __init__(self, default=None): self._default = default self._weakref = WeakKeyDictionary() def __get__(self, instance, objtype): print('{}.__get__({}, {})'.format(self, instance, objtype)) return self._weakref.get(instance, self._default) def __set__(self, instance, value): print('{}.__set__({}, {})'.format(self, instance, value)) self._weakref[instance] = value class Meta(type): x = MetaAttr() class A(metaclass=Meta): pass A.x = 19 # <__main__.MetaAttr object at 0x110fe8e80>.__set__(<class '__main__.A'>, 19)
What? I can have a descriptor in my metaclass!!!