'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 1:[1]

@unutbu's answer is perfectly fine and addressed the main caveats very well.

However, there is one more thing to note (I cannot comment yet so here I am posing an entire answer), note that classproperty's and instanceproperty's have different keys.

What you are doing here is entirely based on class properties. If one wishes to make a setter for an "instance" property the key must be f"_{clsname}__{key}". I lack the technical understanding of Python data model to explain why or if what I'm saying is entirely correct or has other caveats. Just posting this here as a side note.

This is especially important if you wish to allow the user to override the methods with custom ones like the example I provide here (I'm not sure even if it has no syntax error but still I post it here):

class MyMeta(type):
    def __new__(cls, clsname, bases, info, **kwargs):
        property_key = "__my_private"  # should have been f"_{clsname}__my_private"
        info[property_key] = "default_value"
        if "my_getter" not in info:
            info["my_getter"] = lambda self, key=property_key: getattr(self, key)
        return super().__new__(cls, clsname, bases, info, **kwargs)

class MyClass(metaclass=MyMeta):
    def my_getter(self):
        return f"will fail to get {self.__my_private}"

MyClass().my_getter() # => Raises: AttributeError "_MyClass__my_private" not found.
# This would work so fine if no `self` keyword was being used (treated as classproperty)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1