0
\$\begingroup\$

Classes in Python do not have native support for static properties. A meta-class can rather easily add this support as shown below. Are there any problems programmers might experience if they use this implementation?

#! /usr/bin/env python3
class StaticProperty(type):
    def __getattribute__(cls, name):
        attribute = super().__getattribute__(name)
        try:
            return attribute.__get__(cls, type(cls))
        except AttributeError:
            return attribute

    def __setattr__(cls, name, value):
        try:
            super().__getattribute__(name).__set__(cls, value)
        except AttributeError:
            super().__setattr__(name, value)


class Test(metaclass=StaticProperty):
    __static_variable = None

    @property
    def static_variable(cls):
        assert isinstance(cls, StaticProperty)
        return cls.__static_variable

    @static_variable.setter
    def static_variable(cls, value):
        assert isinstance(cls, StaticProperty)
        cls.__static_variable = value

    def __init__(self):
        self.__value = None

    @property
    def value(self):
        assert isinstance(self, Test)
        return self.__value

    @value.setter
    def value(self, value):
        assert isinstance(self, Test)
        self.__value = value


def main():
    print(repr(Test.static_variable))
    Test.static_variable = '1st Hello, world!'
    print(repr(Test.static_variable))

    instance = Test()
    print(repr(instance.value))
    instance.value = '2nd Hello, world!'
    print(repr(instance.value))

    assert Test._Test__static_variable == '1st Hello, world!'
    assert instance._Test__value == '2nd Hello, world!'


if __name__ == '__main__':
    main()

My first inclination is that the property class should be sub-classed as static_property and should be checked for in StaticProperty.__new__ to ensure it is being used properly.

\$\endgroup\$
  • \$\begingroup\$ So, to be clear, you want class properties? Properties that are bound to the class not the instance. \$\endgroup\$ – Peilonrayz Jun 5 at 20:38
  • \$\begingroup\$ @Peilonrayz Yes, but the example code seems to illustrate that both kinds of properties are possible. Some might find it confusing, though, if there is no distinction between a class and instance property. \$\endgroup\$ – Noctis Skytower Jun 5 at 20:42
1
\$\begingroup\$
  • You've masked a bug, in __setattr__ a property raises an AttributeError if the setter hasn't been defined. This causes you to overwrite the property.
  • (As you've said) There's no distinction between a class property and an instance property. You can change it so there is, but it doesn't allow the property to only be defined on the class, not the instance.
  • You can just define the static properties on the metaclass. This removes a lot of the headaches.
  • If you really want to define everything onto the class not the metaclass then you can make the metaclass hoist the wanted functions into a new metaclass. This means everything works as if you only defined two metaclasses with the properties correctly defined.

No fancy changes:

class MyMeta(type):
    @property
    def class_(self):
        return self._class

    @class_.setter
    def class_(self, value):
        self._class = value

    @property
    def class_instance(self):
        return self._class_instance

    @class_instance.setter
    def class_instance(self, value):
        self._class_instance = value

class Test(metaclass=MyMeta):
    class_instance = MyMeta.class_instance

    @property
    def instance(self):
        return self._instance

    @instance.setter
    def instance(self, value):
        self._instance = value

Hoisting:

class classproperty(property):
    pass


class classinstanceproperty(property):
    pass


class StaticProperty(type):
    def __new__(self, name, bases, props):
        class_properties = {}
        to_remove = {}
        for key, value in props.items():
            if isinstance(value, (classproperty, classinstanceproperty)):
                class_properties[key] = value
                if isinstance(value, classproperty):
                    to_remove[key] = value

        for key in to_remove:
            props.pop(key)

        HoistMeta = type('HoistMeta', (type,), class_properties)
        return HoistMeta(name, bases, props)


class Test(metaclass=StaticProperty):
    @classproperty
    def class_(self):
        return self._class

    @class_.setter
    def class_(self, value):
        self._class = value

    @classinstanceproperty
    def class_instance(self):
        return self._class_instance

    @class_instance.setter
    def class_instance(self, value):
        self._class_instance = value

    @property
    def instance(self):
        return self._instance

    @instance.setter
    def instance(self, value):
        self._instance = value

These both pass the following tests: (I could only get your approach to work with instance and class instance)


test = Test()
test._instance = None
test.instance = True
assert test._instance is True
assert test.instance is True
test.instance = False
assert test._instance is False
assert test.instance is False

Test._instance = None
Test.instance = True
Test.instance = False
assert Test._instance is None
test._instance = True
if Test._instance is not True:
    print("instance can't be used after modifying class")

Test._class_instance = None
Test.class_instance = True
assert Test._class_instance is True
Test.class_instance = False
assert Test._class_instance is False

test = Test()
test._class_instance = None
test.class_instance = True
assert test._class_instance is True
assert Test._class_instance is False
test.class_instance = False
assert test._class_instance is False

Test._class = None
Test.class_ = True
assert Test._class is True
Test.class_ = False
assert Test._class is False

test = Test()
test._class = None
test.class_ = True
assert test._class is None
assert Test._class is False
test.class_ = False
assert test._class is None

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.