Search code examples
pythonpropertiesmetaclass

How to add properties to a metaclass instance?


I would like to define metaclass that will enable me to create properties (i.e. setter, getter) in new class on the base of the class's attributes.

For example, I would like to define the class:

class Person(metaclass=MetaReadOnly):
    name = "Ketty"
    age = 22

    def __str__(self):
        return ("Name: " + str(self.name) + "; age: "
                + str(self.age))

But I would like to get something like this:

class Person():
    __name = "Ketty"
    __age = 22

    @property
    def name(self):
        return self.__name;
    @name.setter
    def name(self, value):
        raise RuntimeError("Read only")

    @property
    def age(self):
        return self.__age
    @age.setter
    def age(self, value):
        raise RuntimeError("Read only")

    def __str__(self):
        return ("Name: " + str(self.name) + "; age: "
                + str(self.age))

Here is the metaclass I have written:

class MetaReadOnly(type):
    def __new__(cls, clsname, bases, dct):

        result_dct = {}

        for key, value in dct.items():
            if not key.startswith("__"):
                result_dct["__" + key] = value

                fget = lambda self: getattr(self, "__%s" % key)
                fset = lambda self, value: setattr(self, "__"
                                                   + key, value)

                result_dct[key] = property(fget, fset)

            else:
                result_dct[key] = value

        inst = super(MetaReadOnly, cls).__new__(cls, clsname,
                                                bases, result_dct)

        return inst

    def raiseerror(self, attribute):
        raise RuntimeError("%s is read only." % attribute)

However it dosen't work properly.

client = Person()
print(client)

Sometimes I get:

Name: Ketty; age: Ketty

sometimes:

Name: 22; age: 22

or even an error:

Traceback (most recent call last):
  File "F:\Projects\TestP\src\main.py", line 38, in <module>
    print(client)
  File "F:\Projects\TestP\src\main.py", line 34, in __str__
    return ("Name: " + str(self.name) + "; age: " + str(self.age))
  File "F:\Projects\TestP\src\main.py", line 13, in <lambda>
    fget = lambda self: getattr(self, "__%s" % key)
AttributeError: 'Person' object has no attribute '____qualname__'

I have found example how it can be done other way Python classes: Dynamic properties, but I would like to do it with metaclass. Do you have any idea how this can be done or is it possible at all ?


Solution

  • Bind the current value of key to the parameter in your fget and fset definitions:

    fget = lambda self, k=key: getattr(self, "__%s" % k)
    fset = lambda self, value, k=key: setattr(self, "__" + k, value)
    

    This is a classic Python pitfall. When you define

    fget = lambda self: getattr(self, "__%s" % key)
    

    the value of key is determined when fget is called, not when fget is defined. Since it is a nonlocal variable, its value is found in the enclosing scope of the __new__ function. By the time fget gets called, the for-loop has ended, so the last value of key is the value found. Python3's dict.item method returns items in a unpredictable order, so sometimes the last key is, say, __qualname__, which is why a suprising error is sometimes raised, and sometimes the same wrong value is returned for all attributes with no error.


    When you define a function with a parameter with a default value, the default value is bound to the parameter at the time the function is defined. Thus, the current values of key get corrected bound to fget and fset when you bind default values to k.

    Unlike before, k is now a local variable. The default value is stored in fget.__defaults__ and fset.__defaults__.


    Another option is to use a closure. You can define this outside of the metaclass:

    def make_fget(key):
        def fget(self):
            return getattr(self, "__%s" % key)
        return fget
    def make_fset(key):
        def fset(self, value):
            setattr(self, "__" + key, value)
        return fset
    

    and use it inside the metaclass like this:

    result_dct[key] = property(make_fget(key), make_fset(key))
    

    Now when fget or fset is called, the proper value of key is found in the enclosing scope of make_fget or make_fset.