-2

I've been looking for a way to define class properties in Python.

The expected behavior would be something intuitive like:

class A:
    _access_count = 0
    @classproperty
    def value1(cls):
        cls._access_count += 1
        return 1

A.value1        # return 1
A().value1      # return 1
A._access_count # return 2
A.value1 = 2    # raise an AttributeError

I found related questions on SO, but none of them propose this exact feature.

This thread has a nice example of metaclass even though it doesn't really apply in that case. The accepted answer of this one propose a close solution but doesn't handle the setter mecanism.

Community
  • 1
  • 1
Vincent
  • 12,919
  • 1
  • 42
  • 64

1 Answers1

0

Since it is ok to answer his own question I'll write what I've come up with so far.

class classproperty(property):
    """Class property works exactly like property."""
    pass

def create_instance_property(cls_prop):
    """Create instance property from class property."""
    fget, fset, fdel = None, None, None
    if cls_prop.fget is not None :
        fget = lambda self: cls_prop.fget(type(self))
    if cls_prop.fset is not None :
        fset = lambda self, value: cls_prop.fset(type(self), value)
    if cls_prop.fdel is not None :
        fdel = lambda self: cls_prop.fdel(type(self))
    return property(fget, fset, fdel, cls_prop.__doc__)

def init_for_metaclass(cls, name, bases, dct):
    """__init__ method for a metaclass to handle class properties."""
    super(type(cls), cls).__init__(name, bases, dct)
    for key, prop in dct.items():
        if isinstance(prop, classproperty):
            setattr(cls, key, create_instance_property(prop))
            setattr(type(cls), key, prop)

def handle_class_property(cls):
    """Class decorator to handle class properties."""
    name = type(cls).__name__ + "_for_" + cls.__name__ 
    metacls = type(name, (type(cls),), {"__init__": init_for_metaclass})
    return metacls(cls.__name__, cls.__bases__, dict(cls.__dict__))

So far it works exactly as expected, even for inheritance cases:

@handle_class_property
class A(object):
    _access_count = 0
    
    @classproperty
    def value1(cls):
        print cls
        cls._access_count += 1
        return 1

class B(A):
    _access_count = 0
    
    @classproperty
    def value2(cls):
        cls._access_count += 1
        return 2
    @value2.setter
    def value2(cls, value):
        print(value)

    
    a = A()
    b = B()
    assert (a.value1, A.value1) == (1,1)
    assert (b.value1, B.value1) == (1,1)
    assert (b.value2, B.value2) == (2,2)
    assert  B._access_count == 4
    assert  A._access_count == 2
    B.value2 = 42 # This should print '42'
    try: B.value1 = 42 # This should raise an exception
    except AttributeError as e: print(repr(e))
Community
  • 1
  • 1
Vincent
  • 12,919
  • 1
  • 42
  • 64