Note Updated as requested to shorter code and question. Hope this helps.
I am trying to understand the various ways in which one could create class properties (e.g., like @property for instance attributes, but for class attributes/variables). I've tried out some of the suggestions on here (e.g., Using property() on classmethods and How to make a class property?).
In short, it looks like using the meta-class methodology recommended for Python 3.x in "Using property() on classmethods" results in the attribute actually not being preserved (see test results). I'm wondering if either I made a mistake, or if someone could explain why what I see is the expected and right behavior.
Note that the code from "How to make a class property?" seems to behave as I would expect, but I tested the "best" answer in "How to make a class property?" and it is not behaving as I expected.
The Code
Trying to figure things out, I wrote some test code for the meta-class method specified for Python 3.x:
class SomeClassMeta(type):
meta_attr = "SomeClassMeta Default meta_attr"
# From https://stackoverflow.com/questions/128573/using-property-on-classmethods
def __init__(self, *args, **kwargs):
self.meta_attr = "SomeClassMeta.__init__() set meta_attr"
pass
@property
def meta_prop(self):
return self.meta_attr
@meta_prop.setter
def meta_prop(self, val):
self.meta_attr = val
pass
class ClasspropertyDescriptor(object):
# From https://stackoverflow.com/questions/5189699/how-to-make-a-class-property
def __init__(self, fget, fset=None):
self.fget = fget
self.fset = fset
def __get__(self, obj, klass=None):
"""Get it."""
if klass is None:
klass = type(obj)
return self.fget.__get__(obj, klass)()
def __set__(self, obj, value):
"""Set it."""
if not self.fset:
raise AttributeError("can't set attribute")
type_ = type(obj)
return self.fset.__get__(obj, type_)(value)
def setter(self, func):
"""Set some class value."""
if not isinstance(func, (classmethod, staticmethod)):
func = classmethod(func)
self.fset = func
return self
def classproperty(func):
# From https://stackoverflow.com/questions/5189699/how-to-make-a-class-property
if not isinstance(func, (classmethod, staticmethod)):
func = classmethod(func)
pass
return ClasspropertyDescriptor(func)
class ClasspropertyMetaClass(type):
def __setattr__(self, key, value):
if key in self.__dict__:
obj = self.__dict__.get(key)
if obj and type(obj) is ClasspropertyDescriptor:
return obj.__set__(self, value)
return super(ClasspropertyMetaClass, self).__setattr__(key, value)
class SomeClass(metaclass=SomeClassMeta):
class_attr = "SomeClass Default class_attr"
norm_attr = "SomeClass Default norm_attr"
inst_attr = "SomeClass Default inst_attr"
_name = "SomeClass"
def __init__(self,name):
"""Init this."""
self.inst_attr = "SomeClass.__init__() set inst_attr"
self._name = name
pass
@property
def norm_prop(self):
return self.norm_attr
@norm_prop.setter
def norm_prop(self, val):
self.norm_attr = val
pass
@classproperty
def class_prop(self):
return self.class_attr
@class_prop.setter
def class_prop(self, val):
self.class_attr = val
pass
@property
def inst_prop(self):
"""Get the instance variable (attribute)."""
return self.inst_attr
@inst_prop.setter
def inst_prop(self, val):
self.inst_attr = val
pass
def _info(self,attr):
attrval = getattr(self,attr,'No Such Attribute')
attrcval = getattr(self.__class__,attr,'No Such Attribute')
print(f" - {self._name}.{attr} = '{attrval}', {self._name}.__class__.{attr} = '{attrcval}'")
if isinstance(attrval,property):
print(f" - {self._name}.{attr}.__get__() = '{attrval.__get__(self)}'")
def info(self):
print(f"{self._name} is a {type(self).__name__}")
for attr in [
'class_prop',
'class_attr',
'inst_attr',
'inst_prop',
'meta_attr',
'meta_prop',
'norm_attr',
'norm_prop',
]:
self._info(attr)
self.__class__._info(self.__class__,attr)
Some_Inst = SomeClass('Some_Inst')
Some_Inst.class_prop = "Set with Some_Inst.class_prop"
Some_Inst.inst_prop = "Set with Some_Inst.inst_prop"
Some_Inst.meta_prop = "Set with Some_Inst.meta_prop"
Some_Inst.norm_prop = "Set with Some_Inst.norm_prop"
Some_Inst.info()
The Output
Some_Inst is a SomeClass
- Some_Inst.class_prop = 'Set with Some_Inst.class_prop', Some_Inst.__class__.class_prop = 'Set with Some_Inst.class_prop'
- SomeClass.class_prop = 'Set with Some_Inst.class_prop', SomeClass.__class__.class_prop = 'No Such Attribute'
- Some_Inst.class_attr = 'Set with Some_Inst.class_prop', Some_Inst.__class__.class_attr = 'Set with Some_Inst.class_prop'
- SomeClass.class_attr = 'Set with Some_Inst.class_prop', SomeClass.__class__.class_attr = 'No Such Attribute'
- Some_Inst.inst_attr = 'Set with Some_Inst.inst_prop', Some_Inst.__class__.inst_attr = 'SomeClass Default inst_attr'
- SomeClass.inst_attr = 'SomeClass Default inst_attr', SomeClass.__class__.inst_attr = 'No Such Attribute'
- Some_Inst.inst_prop = 'Set with Some_Inst.inst_prop', Some_Inst.__class__.inst_prop = '<property object at 0x7fdc48c594a8>'
- SomeClass.inst_prop = '<property object at 0x7fdc48c594a8>', SomeClass.__class__.inst_prop = 'No Such Attribute'
- SomeClass.inst_prop.__get__() = 'SomeClass Default inst_attr'
- Some_Inst.meta_attr = 'SomeClassMeta.__init__() set meta_attr', Some_Inst.__class__.meta_attr = 'SomeClassMeta.__init__() set meta_attr'
- SomeClass.meta_attr = 'SomeClassMeta.__init__() set meta_attr', SomeClass.__class__.meta_attr = 'SomeClassMeta Default meta_attr'
- Some_Inst.meta_prop = 'Set with Some_Inst.meta_prop', Some_Inst.__class__.meta_prop = 'SomeClassMeta.__init__() set meta_attr'
- SomeClass.meta_prop = 'SomeClassMeta.__init__() set meta_attr', SomeClass.__class__.meta_prop = '<property object at 0x7fdc48c59908>'
- Some_Inst.norm_attr = 'Set with Some_Inst.norm_prop', Some_Inst.__class__.norm_attr = 'SomeClass Default norm_attr'
- SomeClass.norm_attr = 'SomeClass Default norm_attr', SomeClass.__class__.norm_attr = 'No Such Attribute'
- Some_Inst.norm_prop = 'Set with Some_Inst.norm_prop', Some_Inst.__class__.norm_prop = '<property object at 0x7fdc48c596d8>'
- SomeClass.norm_prop = '<property object at 0x7fdc48c596d8>', SomeClass.__class__.norm_prop = 'No Such Attribute'
- SomeClass.norm_prop.__get__() = 'SomeClass Default norm_attr'
Questions
- Why do
SomeClass.inst_prop
andSomeClass.norm_prop
return aproperty()
while all otherproperty()
s whether on the class or instance behave as expected (even after first instantiation)? - I thought the purpose of the metaclass was to create a Class property. Why then does setting
Some_Inst.meta_prop = "Set with Some_Inst.meta_prop"
change the instance value but not the class value? Note that Some_Inst.class_prop behaves as I thought it would.