Class attributes

Posted 3 months, 3 weeks ago | Originally written on 9 Mar 2020

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!!!