How to create a read-only class property in Python?

38,259

Solution 1

The property descriptor always returns itself when accessed from a class (ie. when instance is None in its __get__ method).

If that's not what you want, you can write a new descriptor that always uses the class object (owner) instead of the instance:

>>> class classproperty(object):
...     def __init__(self, getter):
...         self.getter= getter
...     def __get__(self, instance, owner):
...         return self.getter(owner)
... 
>>> class Foo(object):
...     x= 4
...     @classproperty
...     def number(cls):
...         return cls.x
... 
>>> Foo().number
4
>>> Foo.number
4

Solution 2

This will make Foo.number a read-only property:

class MetaFoo(type):
    @property
    def number(cls):
        return cls.x

class Foo(object, metaclass=MetaFoo):
    x = 4

print(Foo.number)
# 4

Foo.number = 6
# AttributeError: can't set attribute

Explanation: The usual scenario when using @property looks like this:

class Foo(object):
    @property
    def number(self):
        ...
foo = Foo()

A property defined in Foo is read-only with respect to its instances. That is, foo.number = 6 would raise an AttributeError.

Analogously, if you want Foo.number to raise an AttributeError you would need to setup a property defined in type(Foo). Hence the need for a metaclass.


Note that this read-onlyness is not immune from hackers. The property can be made writable by changing Foo's class:

class Base(type): pass
Foo.__class__ = Base

# makes Foo.number a normal class attribute
Foo.number = 6   
print(Foo.number)

prints

6

or, if you wish to make Foo.number a settable property,

class WritableMetaFoo(type): 
    @property
    def number(cls):
        return cls.x
    @number.setter
    def number(cls, value):
        cls.x = value
Foo.__class__ = WritableMetaFoo

# Now the assignment modifies `Foo.x`
Foo.number = 6   
print(Foo.number)

also prints

6

Solution 3

I agree with unubtu's answer; it seems to work, however, it doesn't work with this precise syntax on Python 3 (specifically, Python 3.4 is what I struggled with). Here's how one must form the pattern under Python 3.4 to make things work, it seems:

class MetaFoo(type):
   @property
   def number(cls):
      return cls.x

class Foo(metaclass=MetaFoo):
   x = 4

print(Foo.number)
# 4

Foo.number = 6
# AttributeError: can't set attribute

Solution 4

The solution of Mikhail Gerasimov is quite complete. Unfortunately, it was one drawback. If you have a class using his classproperty, no child class can use it due to an TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases with class Wrapper.

Fortunately, this can be fixed. Just inherit from the metaclass of the given class when creating class Meta.

def classproperty_support(cls):
  """
  Class decorator to add metaclass to our class.
  Metaclass uses to add descriptors to class attributes, see:
  http://stackoverflow.com/a/26634248/1113207
  """
  # Use type(cls) to use metaclass of given class
  class Meta(type(cls)): 
      pass

  for name, obj in vars(cls).items():
      if isinstance(obj, classproperty):
          setattr(Meta, name, property(obj.fget, obj.fset, obj.fdel))

  class Wrapper(cls, metaclass=Meta):
      pass
  return Wrapper

Solution 5

Problem with solutions above is that it wouldn't work for accessing class variables from instance variable:

print(Foo.number)
# 4

f = Foo()
print(f.number)
# 'Foo' object has no attribute 'number'

Moreover, using metaclass explicit is not so nice, as using regular property decorator.

I tried to solve this problems. Here how it works now:

@classproperty_support
class Bar(object):
    _bar = 1

    @classproperty
    def bar(cls):
        return cls._bar

    @bar.setter
    def bar(cls, value):
        cls._bar = value


# @classproperty should act like regular class variable.
# Asserts can be tested with it.
# class Bar:
#     bar = 1


assert Bar.bar == 1

Bar.bar = 2
assert Bar.bar == 2

foo = Bar()
baz = Bar()
assert foo.bar == 2
assert baz.bar == 2

Bar.bar = 50
assert baz.bar == 50
assert foo.bar == 50

As you see, we have @classproperty that works same way as @property for class variables. Only thing we will need is additional @classproperty_support class decorator.

Solution also works for read-only class properties.

Here's implementation:

class classproperty:
    """
    Same as property(), but passes obj.__class__ instead of obj to fget/fset/fdel.
    Original code for property emulation:
    https://docs.python.org/3.5/howto/descriptor.html#properties
    """
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj.__class__)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj.__class__, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj.__class__)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)


def classproperty_support(cls):
    """
    Class decorator to add metaclass to our class.
    Metaclass uses to add descriptors to class attributes, see:
    http://stackoverflow.com/a/26634248/1113207
    """
    class Meta(type):
        pass

    for name, obj in vars(cls).items():
        if isinstance(obj, classproperty):
            setattr(Meta, name, property(obj.fget, obj.fset, obj.fdel))

    class Wrapper(cls, metaclass=Meta):
        pass
    return Wrapper

Note: code isn't tested much, feel free to note if it doesn't work as you expect.

Share:
38,259
Björn Pollex
Author by

Björn Pollex

I believe that there is some answer to any question. It is not the task of the users here to lecture others or criticize them for laziness or stupidity. There is little educational value in sarcasm. If a question is badly formulated, one can give advice on how to do better. If important information is missing, one can kindly ask for it. Someone else being unfriendly or badly mannered is no justification for being so yourself. Before posting any question about a problem with your code, please read this (twice). Favorite Answers C++ - How to refer to recursive structs through pointers using vectors

Updated on July 05, 2022

Comments

  • Björn Pollex
    Björn Pollex almost 2 years

    Essentially I want to do something like this:

    class foo:
        x = 4
        @property
        @classmethod
        def number(cls):
            return x
    

    Then I would like the following to work:

    >>> foo.number
    4
    

    Unfortunately, the above doesn't work. Instead of given me 4 it gives me <property object at 0x101786c58>. Is there any way to achieve the above?

  • HardlyKnowEm
    HardlyKnowEm over 11 years
    This isn't a read-only attribute. Python descriptors intentionally do not intercept class-level calls to setattr and delattr with their __set__ and __delete__ methods. So Foo.number = 6 could still work.
  • user2284570
    user2284570 over 7 years
    @unutbu : Is it possible to perform the reverse : that is make Foo.number writable without rebuilding the Foo class.
  • unutbu
    unutbu over 7 years
    @user2284570: You can make Foo.number writable by changing Foo's metaclass from MetaFoo to something else. You could either make number a setable property or simply not mention it, in which case setting Foo.number would make Foo.number a normal class attribute. I've added some code above to show what I mean.
  • user2284570
    user2284570 over 7 years
    @unutbu : And would I be able to change it’s metaclass back ? Would this work for code objects ?