Dusan Krantic
Dusan Krantic

Reputation: 117

python3 implementing a data descriptor in metaclass

What is the correct way to implement a data descriptor inside a metaclass? In the following (trivial) example, I wish to always append a question mark to the desired value before setting it:

class AddQDesc:

  def __init__ (self, name):
    self.name = name
  
  def __get__ (self, instance, owner=None):
    obj = instance if instance != None else owner
    return obj.__dict__[self.name]
  
  def __set__ (self, instance, value):
    # What should go here ?
    #setattr(instance, self.name, "{}?".format(value))  <- This gives me recursion error
    #instance.__dict__[self.name] = "{}?".format(value) <- This gives me proxymapping error
    pass

class Meta (type):
  var = AddQDesc("var")

class C (metaclass=Meta):
  var = 5

print(C.var)
C.var = 1
print(C.var)

First, it looks like the descriptor was not used when I initialized var to 5. Can I somehow apply descriptor protocol here as well? (Make it "5?") Second, how should the value be updated in the __set__ method? Updating the __dict__ gives me "TypeError: 'mappingproxy' object does not support item assignment" and using setattr gives me "RecursionError: maximum recursion depth exceeded while calling a Python object".

Upvotes: 1

Views: 282

Answers (1)

jsbueno
jsbueno

Reputation: 110261

As I commented in the question, this is tricky - because there is no way from Python code to change a class' __dict__ attribute directly - one have to call setattr and let Python set a class attribute - and, setattr will "see" the descriptor in the metaclass, and call its __set__ instead of modifying the value in the class __dict__ itself. So, you get an infinite recursion loop.

Therefore, if you really require that the attribute proxied by the descriptor will "live" with the same name in the class'dict, you have to resort to: when setting the value, temporarily remove the descriptor from the metaclass, call setattr to set the value, and then restoring it back.

Also, if you want the values set in the class body to be handled through the descriptor, they have to be set with setattr after the class is created - type.__new__ won't check for the descriptor as it builds the initial class __dict__.

from threading import Lock

class AddQDesc:

    def __init__ (self, name):
        self.name = name
        self.lock = Lock()
    
    def __get__ (self, instance, owner=None):
        obj = instance if instance != None else owner
        return obj.__dict__[self.name]
    
    def __set__ (self, instance, value):
        owner_metaclass = type(instance)
        
        with self.lock:
            # Temporarily remove the descriptor to avoid recursion problems
            try:
                # Note that a metaclass-inheritance hierarchy, where
                # the descriptor might be placed in a superclass
                # of the instance's metaclass, is not handled here.
                delattr(owner_metaclass, self.name)
                setattr(instance, self.name, value + 1)
            finally:
                setattr(owner_metaclass, self.name, self)
      

class Meta (type):
    def __new__(mcls, name, bases, namespace):
        post_init = {}
        for key, value in list(namespace.items()):
            if isinstance(getattr(mcls, key, None), AddQDesc):
                post_init[key] = value
                del namespace[key]
        cls = super().__new__(mcls, name, bases, namespace)
        for key, value in post_init.items():
            setattr(cls, key, value)
        return cls
    
                
    var = AddQDesc("var")

class C (metaclass=Meta):
    var = 5

print(C.var)
C.var = 1
print(C.var)

If you don't need the value to live in the class' __dict__, I'd suggest just storing it elsewhere - a dictionary in the descriptor instance for example, having the classes as keys, will suffice - and will be far less weird.


class AddQDesc:

    def __init__ (self, name):
        self.name = name
        self.storage = {}
        
    def __get__ (self, instance, owner):
        if not instance: return self
        return self.storage[instance]
            
    
    def __set__ (self, instance, value):
        self.storage[instance] = value + 1

Upvotes: 2

Related Questions