Python - metaclass - adding properties

I would like to define a metaclass that will allow me to create properties (i.e. setter, getter) in a new class based on the attributes of the class.

For example, I would like to define a 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 wrote:

 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 does not 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 found an example of how this can be done in a different way ( Python classes: dynamic properties ), but I would like to do it using a metaclass, do you know how this can be done, or is this possible at all?

+8
python metaclass
source share
1 answer

Bind the current key value to a parameter in the fget and fset :

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

This is a classic Python trap. When you define

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

the key value is determined when fget called, and not when fget defined. Since this is a non-local variable, its value is in the scope of the __new__ function. By the time fget is fget , the for-loop , so the last key value is the value found. The Python3 dict.item method returns elements in an unpredictable order, so sometimes the last key, say __qualname__ , so sometimes an increased error occurs, and sometimes the same incorrect value is returned for all attributes without an error.


When you define a function with a parameter with a default value, the default value is bound to the parameter during function definition. Thus, the current key values ​​are corrected with a binding to fget and fset when you bind the default values ​​to k .

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


Another option is to use closure . You can define this outside 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 as follows:

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

Now when fget or fset , the correct key value is in the scope of make_fget or make_fset .

+11
source share

All Articles