I have a class with two class methods (using the classmethod() function) for getting and setting what is essentially a static variable. I tried to use the property() functi
Here is my solution that also caches the class property
class class_property(object):
# this caches the result of the function call for fn with cls input
# use this as a decorator on function methods that you want converted
# into cached properties
def __init__(self, fn):
self._fn_name = fn.__name__
if not isinstance(fn, (classmethod, staticmethod)):
fn = classmethod(fn)
self._fn = fn
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if (
self._fn_name in vars(cls) and
type(vars(cls)[self._fn_name]).__name__ != "class_property"
):
return vars(cls)[self._fn_name]
else:
value = self._fn.__get__(obj, cls)()
setattr(cls, self._fn_name, value)
return value
A property is created on a class but affects an instance. So if you want a classmethod property, create the property on the metaclass.
>>> class foo(object):
... _var = 5
... class __metaclass__(type): # Python 2 syntax for metaclasses
... pass
... @classmethod
... def getvar(cls):
... return cls._var
... @classmethod
... def setvar(cls, value):
... cls._var = value
...
>>> foo.__metaclass__.var = property(foo.getvar.im_func, foo.setvar.im_func)
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3
But since you're using a metaclass anyway, it will read better if you just move the classmethods in there.
>>> class foo(object):
... _var = 5
... class __metaclass__(type): # Python 2 syntax for metaclasses
... @property
... def var(cls):
... return cls._var
... @var.setter
... def var(cls, value):
... cls._var = value
...
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3
or, using Python 3's metaclass=...
syntax, and the metaclass defined outside of the foo
class body, and the metaclass responsible for setting the initial value of _var
:
>>> class foo_meta(type):
... def __init__(cls, *args, **kwargs):
... cls._var = 5
... @property
... def var(cls):
... return cls._var
... @var.setter
... def var(cls, value):
... cls._var = value
...
>>> class foo(metaclass=foo_meta):
... pass
...
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3
Give this a try, it gets the job done without having to change/add a lot of existing code.
>>> class foo(object):
... _var = 5
... def getvar(cls):
... return cls._var
... getvar = classmethod(getvar)
... def setvar(cls, value):
... cls._var = value
... setvar = classmethod(setvar)
... var = property(lambda self: self.getvar(), lambda self, val: self.setvar(val))
...
>>> f = foo()
>>> f.var
5
>>> f.var = 3
>>> f.var
3
The property
function needs two callable
arguments. give them lambda wrappers (which it passes the instance as its first argument) and all is well.
Reading the Python 2.2 release notes, I find the following.
The get method [of a property] won't be called when the property is accessed as a class attribute (C.x) instead of as an instance attribute (C().x). If you want to override the __get__ operation for properties when used as a class attribute, you can subclass property - it is a new-style type itself - to extend its __get__ method, or you can define a descriptor type from scratch by creating a new-style class that defines __get__, __set__ and __delete__ methods.
NOTE: The below method doesn't actually work for setters, only getters.
Therefore, I believe the prescribed solution is to create a ClassProperty as a subclass of property.
class ClassProperty(property):
def __get__(self, cls, owner):
return self.fget.__get__(None, owner)()
class foo(object):
_var=5
def getvar(cls):
return cls._var
getvar=classmethod(getvar)
def setvar(cls,value):
cls._var=value
setvar=classmethod(setvar)
var=ClassProperty(getvar,setvar)
assert foo.getvar() == 5
foo.setvar(4)
assert foo.getvar() == 4
assert foo.var == 4
foo.var = 3
assert foo.var == 3
However, the setters don't actually work:
foo.var = 4
assert foo.var == foo._var # raises AssertionError
foo._var
is unchanged, you've simply overwritten the property with a new value.
You can also use ClassProperty
as a decorator:
class foo(object):
_var = 5
@ClassProperty
@classmethod
def var(cls):
return cls._var
@var.setter
@classmethod
def var(cls, value):
cls._var = value
assert foo.var == 5
Setting it only on the meta class doesn't help if you want to access the class property via an instantiated object, in this case you need to install a normal property on the object as well (which dispatches to the class property). I think the following is a bit more clear:
#!/usr/bin/python
class classproperty(property):
def __get__(self, obj, type_):
return self.fget.__get__(None, type_)()
def __set__(self, obj, value):
cls = type(obj)
return self.fset.__get__(None, cls)(value)
class A (object):
_foo = 1
@classproperty
@classmethod
def foo(cls):
return cls._foo
@foo.setter
@classmethod
def foo(cls, value):
cls.foo = value
a = A()
print a.foo
b = A()
print b.foo
b.foo = 5
print a.foo
A.foo = 10
print b.foo
print A.foo
Old question, lots of views, sorely in need of a one-true Python 3 way.
Luckily, it's easy with the metaclass
kwarg:
class FooProperties(type):
@property
def var(cls):
return cls._var
class Foo(object, metaclass=FooProperties):
_var = 'FOO!'
Then, >>> Foo.var
'FOO!'