Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add properties to a metaclass instance?

Tags:

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 ?

like image 702
hakubaa Avatar asked Dec 24 '14 00:12

hakubaa


People also ask

How do I change metaclass in Python?

In order to set metaclass of a class, we use the __metaclass__ attribute. Metaclasses are used at the time the class is defined, so setting it explicitly after the class definition has no effect. The best idea I can think of is to re-define whole class and add the __metaclass__ attribute dynamically somehow.

What is __ metaclass __ in Python?

A metaclass in Python is a class of a class that defines how a class behaves. A class is itself an instance of a metaclass. A class in Python defines how the instance of the class will behave. In order to understand metaclasses well, one needs to have prior experience working with Python classes.

How do you use metaclass in Python?

To create your own metaclass in Python you really just want to subclass type . A metaclass is most commonly used as a class-factory. When you create an object by calling the class, Python creates a new class (when it executes the 'class' statement) by calling the metaclass.

What is metaclass in OOP?

In object-oriented programming, a metaclass is a class whose instances are classes. Just as an ordinary class defines the behavior of certain objects, a metaclass defines the behavior of certain classes and their instances. Not all object-oriented programming languages support metaclasses.


1 Answers

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.

like image 109
unutbu Avatar answered Oct 05 '22 21:10

unutbu