0

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 and SomeClass.norm_prop return a property() while all other property()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.
Gabe
  • 131
  • 1
  • 13

1 Answers1

1

Since you changed the question substantially, here's a new answer.

- Some_Inst.inst_prop = 'Set with Some_Inst.inst_prop', Some_Inst.__class__.inst_prop = '<property object at 0x000002062A3C9B38>'

As a property of the instance Some_Inst, evaluating Some_Inst.inst_prop gets you the property value, while evaluating Some_Inst.__class__.inst_prop get you what inst_prop is for the class: a property. It's a property defined on the class, so it will resolve to its value for the instance and to a property for the class.

The same is true for norm_prop in the context of your first question.

As for the second question: "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?"

Because Some_class does not have the attribute meta_prop until after you assigned it with Some_Inst.meta_prop = "Set with Some_Inst.meta_prop". meta_prop is a property you defined on the meta-class, but since SomeClass does not inherited from SomeClassMeta (it just has it as a meta-class), it's not on SomeClass.

Why would you want to use meta-classes to define class-properties, why not simply define them on the class? In the words of Tim Peters:

"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why)."

This article has excellent further explanation: https://realpython.com/python-metaclasses/

Grismar
  • 27,561
  • 4
  • 31
  • 54
  • Thank for the explanation. The answer to "Why would you want to use meta-classes to define class-properties" is that I had never considered that before, but the most linked to article on creating a "class property" (https://stackoverflow.com/questions/5189699/how-to-make-a-class-property) has that as the top solution. – Gabe Feb 21 '20 at 16:11
  • I see, thanks for the link. I think the person asking that question would have to answer the same underlying question: why do you need a "`classproperty`" to begin with? In the end, changing how your code behaves from how a normal Python solution behaves using metaclasses is generally a bad idea, since nobody using your code would expect that kind of solution and there are generally better ways of achieving the same. There's always exceptions, but like the quote suggests: you'll know when you have one. – Grismar Feb 21 '20 at 21:20
  • Does the above answer your question though? (I feel it does) – Grismar Feb 21 '20 at 21:20
  • 1
    yes it does and makes more sense than the other stuff floating around the "interwebs". – Gabe Feb 24 '20 at 17:47