Using property() on classmethods

2019-01-02 14:53发布

问题:

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() function with these, but it results in an error. I was able to reproduce the error with the following in the interpreter:

class Foo(object):
    _var = 5
    @classmethod
    def getvar(cls):
        return cls._var
    @classmethod
    def setvar(cls, value):
        cls._var = value
    var = property(getvar, setvar)

I can demonstrate the class methods, but they don't work as properties:

>>> f = Foo()
>>> f.getvar()
5
>>> f.setvar(4)
>>> f.getvar()
4
>>> f.var
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'classmethod' object is not callable
>>> f.var=5
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'classmethod' object is not callable

Is it possible to use the property() function with classmethod decorated functions?

回答1:

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


回答2:

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
...     def getvar(cls):
...         return cls._var
...     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):
...         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


回答3:

I hope this dead-simple read-only @classproperty decorator would help somebody looking for classproperties.

class classproperty(object):

    def __init__(self, fget):
        self.fget = fget

    def __get__(self, owner_self, owner_cls):
        return self.fget(owner_cls)

class C(object):

    @classproperty
    def x(cls):
        return 1

assert C.x == 1
assert C().x == 1


回答4:

Python 3!

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!'



回答5:

Is it possible to use the property() function with classmethod decorated functions?

No.

However, a classmethod is simply a bound method (a partial function) on a class accessible from instances of that class.

Since the instance is a function of the class and you can derive the class from the instance, you can can get whatever desired behavior you might want from a class-property with property:

class Example(object):
    _class_property = None
    @property
    def class_property(self):
        return self._class_property
    @class_property.setter
    def class_property(self, value):
        type(self)._class_property = value
    @class_property.deleter
    def class_property(self):
        del type(self)._class_property

This code can be used to test - it should pass without raising any errors:

ex1 = Example()
ex2 = Example()
ex1.class_property = None
ex2.class_property = 'Example'
assert ex1.class_property is ex2.class_property
del ex2.class_property
assert not hasattr(ex1, 'class_property')

And note that we didn't need metaclasses at all - and you don't directly access a metaclass through its classes' instances anyways.

writing a @classproperty decorator

You can actually create a classproperty decorator in just a few lines of code by subclassing property (it's implemented in C, but you can see equivalent Python here):

class classproperty(property):
    def __get__(self, obj, objtype=None):
        return super(classproperty, self).__get__(objtype)
    def __set__(self, obj, value):
        super(classproperty, self).__set__(type(obj), value)
    def __delete__(self, obj):
        super(classproperty, self).__delete__(type(obj))

Then treat the decorator as if it were a classmethod combined with property:

class Foo(object):
    _bar = 5
    @classproperty
    def bar(cls):
        """this is the bar attribute - each subclass of Foo gets its own.
        Lookups should follow the method resolution order.
        """
        return cls._bar
    @bar.setter
    def bar(cls, value):
        cls._bar = value
    @bar.deleter
    def bar(cls):
        del cls._bar

And this code should work without errors:

def main():
    f = Foo()
    print(f.bar)
    f.bar = 4
    print(f.bar)
    del f.bar
    try:
        f.bar
    except AttributeError:
        pass
    else:
        raise RuntimeError('f.bar must have worked - inconceivable!')
    help(f)  # includes the Foo.bar help.
    f.bar = 5

    class Bar(Foo):
        "a subclass of Foo, nothing more"
    help(Bar) # includes the Foo.bar help!
    b = Bar()
    b.bar = 'baz'
    print(b.bar) # prints baz
    del b.bar
    print(b.bar) # prints 5 - looked up from Foo!


if __name__ == '__main__':
    main()

But I'm not sure how well-advised this would be. An old mailing list article suggests it shouldn't work.

Getting the property to work on the class:

The downside of the above is that the "class property" isn't accessible from the class, because it would simply overwrite the data descriptor from the class __dict__.

However, we can override this with a property defined in the metaclass __dict__. For example:

class MetaWithFooClassProperty(type):
    @property
    def foo(cls):
        """The foo property is a function of the class -
        in this case, the trivial case of the identity function.
        """
        return cls

And then a class instance of the metaclass could have a property that accesses the class's property using the principle already demonstrated in the prior sections:

class FooClassProperty(metaclass=MetaWithFooClassProperty):
    @property
    def foo(self):
        """access the class's property"""
        return type(self).foo

And now we see both the instance

>>> FooClassProperty().foo
<class '__main__.FooClassProperty'>

and the class

>>> FooClassProperty.foo
<class '__main__.FooClassProperty'>

have access to the class property.



回答6:

There is no reasonable way to make this "class property" system to work in Python.

Here is one unreasonable way to make it work. You can certainly make it more seamless with increasing amounts of metaclass magic.

class ClassProperty(object):
    def __init__(self, getter, setter):
        self.getter = getter
        self.setter = setter
    def __get__(self, cls, owner):
        return getattr(cls, self.getter)()
    def __set__(self, cls, value):
        getattr(cls, self.setter)(value)

class MetaFoo(type):
    var = ClassProperty('getvar', 'setvar')

class Foo(object):
    __metaclass__ = MetaFoo
    _var = 5
    @classmethod
    def getvar(cls):
        print "Getting var =", cls._var
        return cls._var
    @classmethod
    def setvar(cls, value):
        print "Setting var =", value
        cls._var = value

x = Foo.var
print "Foo.var = ", x
Foo.var = 42
x = Foo.var
print "Foo.var = ", x

The knot of the issue is that properties are what Python calls "descriptors". There is no short and easy way to explain how this sort of metaprogramming works, so I must point you to the descriptor howto.

You only ever need to understand this sort of things if you are implementing a fairly advanced framework. Like a transparent object persistence or RPC system, or a kind of domain-specific language.

However, in a comment to a previous answer, you say that you

need to modify an attribute that in such a way that is seen by all instances of a class, and in the scope from which these class methods are called does not have references to all instances of the class.

It seems to me, what you really want is an Observer design pattern.



回答7:

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


回答8:

Because I need to modify an attribute that in such a way that is seen by all instances of a class, and in the scope from which these class methods are called does not have references to all instances of the class.

Do you have access to at least one instance of the class? I can think of a way to do it then:

class MyClass (object):
    __var = None

    def _set_var (self, value):
        type (self).__var = value

    def _get_var (self):
        return self.__var

    var = property (_get_var, _set_var)

a = MyClass ()
b = MyClass ()
a.var = "foo"
print b.var


回答9:

Half a solution, __set__ on the class does not work, still. The solution is a custom property class implementing both a property and a staticmethod

class ClassProperty(object):
    def __init__(self, fget, fset):
        self.fget = fget
        self.fset = fset

    def __get__(self, instance, owner):
        return self.fget()

    def __set__(self, instance, value):
        self.fset(value)

class Foo(object):
    _bar = 1
    def get_bar():
        print 'getting'
        return Foo._bar

    def set_bar(value):
        print 'setting'
        Foo._bar = value

    bar = ClassProperty(get_bar, set_bar)

f = Foo()
#__get__ works
f.bar
Foo.bar

f.bar = 2
Foo.bar = 3 #__set__ does not


回答10:

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.



回答11:

Here's a solution which should work for both access via the class and access via an instance which uses a metaclass.

In [1]: class ClassPropertyMeta(type):
   ...:     @property
   ...:     def prop(cls):
   ...:         return cls._prop
   ...:     def __new__(cls, name, parents, dct):
   ...:         # This makes overriding __getattr__ and __setattr__ in the class impossible, but should be fixable
   ...:         dct['__getattr__'] = classmethod(lambda cls, attr: getattr(cls, attr))
   ...:         dct['__setattr__'] = classmethod(lambda cls, attr, val: setattr(cls, attr, val))
   ...:         return super(ClassPropertyMeta, cls).__new__(cls, name, parents, dct)
   ...:

In [2]: class ClassProperty(object):
   ...:     __metaclass__ = ClassPropertyMeta
   ...:     _prop = 42
   ...:     def __getattr__(self, attr):
   ...:         raise Exception('Never gets called')
   ...:

In [3]: ClassProperty.prop
Out[3]: 42

In [4]: ClassProperty.prop = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-e2e8b423818a> in <module>()
----> 1 ClassProperty.prop = 1

AttributeError: can't set attribute

In [5]: cp = ClassProperty()

In [6]: cp.prop
Out[6]: 42

In [7]: cp.prop = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-e8284a3ee950> in <module>()
----> 1 cp.prop = 1

<ipython-input-1-16b7c320d521> in <lambda>(cls, attr, val)
      6         # This makes overriding __getattr__ and __setattr__ in the class impossible, but should be fixable
      7         dct['__getattr__'] = classmethod(lambda cls, attr: getattr(cls, attr))
----> 8         dct['__setattr__'] = classmethod(lambda cls, attr, val: setattr(cls, attr, val))
      9         return super(ClassPropertyMeta, cls).__new__(cls, name, parents, dct)

AttributeError: can't set attribute

This also works with a setter defined in the metaclass.



回答12:

After searching different places, I found a method to define a classproperty valid with Python 2 and 3.

from future.utils import with_metaclass

class BuilderMetaClass(type):
    @property
    def load_namespaces(self):
        return (self.__sourcepath__)

class BuilderMixin(with_metaclass(BuilderMetaClass, object)):
    __sourcepath__ = 'sp'        

print(BuilderMixin.load_namespaces)

Hope this can help somebody :)



回答13:

This is not an additional answer, it is just an extension of some of the examples above to demonstrate that the 'setter' behaviour does not work for class properties. I wrote it to help me understand what was going on and thought it might help someone else understand the problem. (Thanks to the authors of lots of these answers, especially @Jason R. Coombs, @Denis Ryzhkov and @Aaron Hall).

# @Aaron Hall's example
class classproperty(property):
    def __get__(self, obj, objtype=None):
        return super(classproperty, self).__get__(objtype)

    def __set__(self, obj, value):
        super(classproperty, self).__set__(type(obj), value)

    def __delete__(self, obj):
        super(classproperty, self).__delete__(type(obj))


class Demo:
    base_value = 100
    via_property_value = None

    @classmethod
    def calc_val(cls):
        return cls.base_value + 1

    @classproperty
    def calc_via_property(cls):
        if cls.via_property_value:
            return cls.base_value + cls.via_property_value
        return cls.base_value

    @calc_via_property.setter
    def set_via_property_value(cls, value):
        # This NEVER gets called
        cls.via_property_value = value


# test it works via a classmethod
assert Demo.calc_val() == 101
assert Demo().calc_val() == 101

# test it works via a 'classproperty'
assert Demo.calc_via_property == 100
assert Demo().calc_via_property == 100

# test is works via a 'classproperty' if the internal value is changed
Demo.via_property_value = 100
assert Demo.calc_via_property == 200
assert Demo().calc_via_property == 200

# test it sets the internal value correctly
# this actually overrides the property definition instead of the internal value
Demo.calc_via_property = 50 
assert Demo.via_property_value == 50 # remains unchanged at 100
assert Demo.calc_via_property == 150 # is now the value 50 rather than a function
assert Demo().calc_via_property == 150 # is now the value 50 rather than a function


回答14:

Here's my suggestion. Don't use class methods.

Seriously.

What's the reason for using class methods in this case? Why not have an ordinary object of an ordinary class?


If you simply want to change the value, a property isn't really very helpful is it? Just set the attribute value and be done with it.

A property should only be used if there's something to conceal -- something that might change in a future implementation.

Maybe your example is way stripped down, and there is some hellish calculation you've left off. But it doesn't look like the property adds significant value.

The Java-influenced "privacy" techniques (in Python, attribute names that begin with _) aren't really very helpful. Private from whom? The point of private is a little nebulous when you have the source (as you do in Python.)

The Java-influenced EJB-style getters and setters (often done as properties in Python) are there to facilitate Java's primitive introspection as well as to pass muster with the static language compiler. All those getters and setters aren't as helpful in Python.



标签: