How to create a read-only class property in Python?
PythonClassPropertiesPython Problem Overview
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?
Python Solutions
Solution 1 - Python
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 2 - Python
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 3 - Python
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 - Python
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.
Solution 5 - Python
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