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 ?
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
.