Usar property () en classmethods

174

Tengo una clase con dos métodos de clase (usando la función classmethod ()) para obtener y establecer lo que es esencialmente una variable estática. Intenté usar la función property () con estos, pero resulta en un error. Pude reproducir el error con lo siguiente en el intérprete:

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

Puedo demostrar los métodos de clase, pero no funcionan como propiedades:

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

¿Es posible usar la función property () con funciones decoradas con método de clase?

Mark Roddy
fuente

Respuestas:

90

Se crea una propiedad en una clase pero afecta a una instancia. Entonces, si desea una propiedad classmethod, cree la propiedad en la metaclase.

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

Pero dado que está utilizando una metaclase de todos modos, se leerá mejor si solo mueve los métodos de clase allí.

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

o, usando la metaclass=...sintaxis de Python 3 , y la metaclase definida fuera del foocuerpo de la clase, y la metaclase responsable de establecer el valor inicial de _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
A. coadyuvante
fuente
1
Esto no parece funcionar para mí en Python 3.2. Si cambio foo .__ metaclass __. Var = property (foo.getvar.im_func, foo.setvar.im_func) a foo .__ metaclass __. Var = property (foo.getvar .__ func__, foo.setvar .__ func__) obtengo "AttributeError: type El objeto 'foo' no tiene el atributo 'var' "al ejecutar" foo.var ".
Michael Kelley
SIGH doble corrección: esto funciona en Python 2.7, pero no en Python 3.2.
Michael Kelley
@MichaelKelley - Eso es porque la sintaxis de las metaclases ha cambiado en Python 3.x
mac
1
No estoy seguro de entender, ¿cuál sería la forma Python 3.x de escribir esto entonces?
SylvainD
8
@Josay: Primero deberías definir la metaclase, luego definir la clase usando la nueva class Foo(metaclass=...)sintaxis.
Kevin
69

Al leer las notas de la versión de Python 2.2 , encuentro lo siguiente.

No se llamará al método get [de una propiedad] cuando se accede a la propiedad como un atributo de clase (Cx) en lugar de como un atributo de instancia (C (). X). Si desea anular la operación __get__ para las propiedades cuando se usa como un atributo de clase, puede subclasificar la propiedad, es un tipo de estilo nuevo, para extender su método __get__, o puede definir un tipo de descriptor desde cero creando un nuevo -style clase que define los métodos __get__, __set__ y __delete__.

NOTA: El siguiente método en realidad no funciona para setters, solo getters.

Por lo tanto, creo que la solución prescrita es crear una ClassProperty como una subclase de propiedad.

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

Sin embargo, los setters en realidad no funcionan:

foo.var = 4
assert foo.var == foo._var # raises AssertionError

foo._var no ha cambiado, simplemente ha sobrescrito la propiedad con un nuevo valor.

También puedes usar ClassPropertycomo decorador:

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
Jason R. Coombs
fuente
20
No creo que la parte de establecimiento de ClassProperty realmente funcione como se describe: mientras que las afirmaciones del ejemplo pasan, al final foo._var == 4 (no 3, como está implícito). Establecer la propiedad registra la propiedad en sí. Cuando se discutieron las propiedades de clase en python-dev , se señaló que, aunque los captadores son triviales, los establecedores son difíciles (¿imposibles?) Sin una metaclase
Gabriel Grant
44
@Gabriel Totalmente correcto. No puedo creer que nadie lo haya señalado durante dos años.
agf
Tampoco estoy seguro de por qué no solo usa self.fget(owner)y elimina la necesidad de tener que usar un @classmethoden absoluto aquí. (eso es lo classmethod que se traduce .__get__(instance, owner)(*args, **kwargs)en function(owner, *args, **kwargs)llamadas a través de un intermediario; las propiedades no necesitan el intermediario)
Martijn Pieters
Su demostración carece de cualquier transformación real, ya sea en el getter o en el setter, que demuestre claramente que su foo.var = 3asignación en realidad no pasa por la propiedad , y en su lugar simplemente ha reemplazado el objeto de propiedad foocon un número entero. Si agrega assert isinstance(foo.__dict__['var'], ClassProperty)llamadas entre sus afirmaciones, verá que falla después de que foo.var = 3se ejecuta.
Martijn Pieters
1
Clases de Python no lo hacen descriptor de apoyo vinculante sobre la configuración de la propia clase , única en conseguir (así instance.attr, instance.attr = valuey del instance.attrse unen todos en el descriptor encontrado type(instance), pero al mismo tiempo classobj.attrse une, classobj.attr = valuey del classobj.attrqué no y en lugar de sustituir o eliminar el objeto descriptor de sí mismo). Necesita una metaclase para admitir la configuración y eliminación (haciendo que el objeto de clase sea la instancia y la metaclase el tipo).
Martijn Pieters
56

Espero que este simple @classpropertydecorador de solo lectura ayude a alguien que busque propiedades de clase.

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
Denis Ryzhkov
fuente
2
¿Funciona esto con subclases? (¿Puede una subclase anular las propiedades de clase?)
zakdances
1
Umm si? class D(C): x = 2; assert D.x == 2
dtheodor
Desearía que esto funcionara cuando lo uso .formatcomo "{x}".format(**dict(self.__class__.__dict__, **self.__dict__)):(
2rs2ts
@Nathan No solo ... cuando lo configura, anula todos los xaccesos 10. Me gusta este enfoque porque es ordenado y simple pero suena como un antipatrón
Michele d'Amico
Fácilmente arreglado: agregue un __set__que genere un ValueErrorpara evitar la anulación.
Kiran Jonnalagadda
30

¿Es posible usar la función property () con funciones decoradas con método de clase?

No.

Sin embargo, un método de clase es simplemente un método enlazado (una función parcial) en una clase accesible desde instancias de esa clase.

Dado que la instancia es una función de la clase y puede derivar la clase de la instancia, puede obtener el comportamiento deseado que desee de una propiedad de clase con 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

Este código se puede usar para probar, debe pasar sin generar ningún error:

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

Y tenga en cuenta que no necesitábamos metaclases en absoluto, y de todos modos no accede directamente a una metaclase a través de las instancias de sus clases.

escribiendo un @classpropertydecorador

En realidad, puede crear un classpropertydecorador en solo unas pocas líneas de código subclasificando property(está implementado en C, pero puede ver Python equivalente aquí ):

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))

Luego trate al decorador como si fuera un método de clase combinado con una propiedad:

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

Y este código debería funcionar sin errores:

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()

Pero no estoy seguro de lo bien aconsejado que sería. Un viejo artículo de la lista de correo sugiere que no debería funcionar.

Conseguir que la propiedad funcione en la clase:

La desventaja de lo anterior es que la "propiedad de clase" no es accesible desde la clase, porque simplemente sobrescribiría el descriptor de datos de la clase __dict__.

Sin embargo, podemos anular esto con una propiedad definida en la metaclase __dict__. Por ejemplo:

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

Y luego una instancia de clase de la metaclase podría tener una propiedad que acceda a la propiedad de la clase utilizando el principio ya demostrado en las secciones anteriores:

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

Y ahora vemos tanto la instancia

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

y la clase

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

tener acceso a la propiedad de la clase.

Aaron Hall
fuente
3
Claro, conciso, minucioso: esta debería ser la respuesta aceptada.
pfabri
25

Python 3!

Antigua pregunta, muchas vistas, que necesitan urgentemente una forma única de Python 3.

Afortunadamente, es fácil con el metaclasskwarg:

class FooProperties(type):

    @property
    def var(cls):
        return cls._var

class Foo(object, metaclass=FooProperties):
    _var = 'FOO!'

Luego, >>> Foo.var

'FOO!'

OJFord
fuente
1
es decir, no hay una forma simple de salir de la caja
mehmet
@mehmet ¿No es esto simple? Fooes una instancia de su metaclase, y @propertypuede usarse para sus métodos de la misma manera que para las instancias de Foo.
OJFord
2
tenía que definir otra clase para una clase, que es el doble de complejidad suponiendo que la metaclase no sea reutilizable.
mehmet
Un método de clase funciona tanto desde la clase como desde la instancia. Esta propiedad solo funciona desde la clase. No creo que esto sea lo que se pide.
Aaron Hall
1
@AaronHall Si eso es importante, se agrega fácilmente Foo.__new__. Aunque en ese momento puede valer la pena usar getattribute en su lugar, o cuestionarse si pretender que existe una característica del lenguaje es realmente el enfoque que desea adoptar.
OJFord
16

No hay una forma razonable de hacer que este sistema de "propiedad de clase" funcione en Python.

Aquí hay una forma irrazonable de hacerlo funcionar. Ciertamente puedes hacerlo más fluido con cantidades crecientes de metaclases mágicas.

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

El nudo del problema es que las propiedades son lo que Python llama "descriptores". No hay una manera corta y fácil de explicar cómo funciona este tipo de metaprogramación, por lo que debo señalarle el descriptor .

Solo necesita comprender este tipo de cosas si está implementando un marco bastante avanzado. Como una persistencia de objetos transparentes o un sistema RPC, o un tipo de lenguaje específico de dominio.

Sin embargo, en un comentario a una respuesta anterior, usted dice que

necesita modificar un atributo que de tal manera que sea visto por todas las instancias de una clase, y en el ámbito desde el que se llama a estos métodos de clase no tiene referencias a todas las instancias de la clase.

Me parece que lo que realmente quieres es un patrón de diseño Observer .

ddaa
fuente
Me gusta la idea del ejemplo de código, pero parece que sería un poco torpe en la práctica.
Mark Roddy
Lo que estoy tratando de lograr es establecer y obtener un solo atributo que se usa como indicador para modificar el comportamiento de todas las instancias, así que creo que Observer sería exagerado por lo que estoy tratando de hacer. Si hubiera múltiples atributos en cuestión, estaría más inclinado.
Mark Roddy
Parece que simplemente hacer públicas las funciones y llamarlas directamente fue la solución simplista. Tenía curiosidad si estaba haciendo algo mal o si estaba tratando de hacerlo era imposible. Perdón por los múltiples comentarios por cierto. El límite de 300 caracteres apesta.
Mark Roddy
Lo ingenioso del ejemplo de código es que puede implementar todos los bits torpes una vez y luego heredarlos. Simplemente mueva el _var a la clase derivada. clase D1 (Foo): _var = 21 clase D2 (Foo) _var = "Hola" D1.var 21 D2.var Hola
Thomas L Holaday
6

Establecerlo solo en la metaclase no ayuda si desea acceder a la propiedad de la clase a través de un objeto instanciado, en este caso también necesita instalar una propiedad normal en el objeto (que se envía a la propiedad de la clase). Creo que lo siguiente es un poco más claro:

#!/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
Nils Philippsen
fuente
3

La mitad de una solución, __set__ en la clase aún no funciona. La solución es una clase de propiedad personalizada que implementa tanto una propiedad como un método estático

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
Florian Bösch
fuente
3

Porque necesito modificar un atributo que de tal manera que sea visto por todas las instancias de una clase, y en el ámbito desde el cual se llaman estos métodos de clase no tiene referencias a todas las instancias de la clase.

¿Tiene acceso a al menos una instancia de la clase? Entonces puedo pensar en una forma de hacerlo:

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
John Millikin
fuente
2

Intente esto, hace el trabajo sin tener que cambiar / agregar mucho código existente.

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

La propertyfunción necesita dos callableargumentos. darles envoltorios lambda (que pasa la instancia como su primer argumento) y todo está bien.

Sufí
fuente
Como señala Florian Bösch, la sintaxis requerida (por bibliotecas de terceros o código heredado) es foo.var.
Thomas L Holaday
2

Aquí hay una solución que debería funcionar para el acceso a través de la clase y el acceso a través de una instancia que utiliza una metaclase.

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

Esto también funciona con un setter definido en la metaclase.

grulla de papel
fuente
1

Después de buscar en diferentes lugares, encontré un método para definir una propiedad de clase válida con Python 2 y 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)

Espero que esto pueda ayudar a alguien :)

no revelado
fuente
1
Si este método es algo que encontró en algún lugar, sería bueno darle un enlace (consulte Cómo hacer referencia al material escrito por otros )
Andrew Myers
-27

Aquí está mi sugerencia. No uses métodos de clase.

Seriamente.

¿Cuál es la razón para usar métodos de clase en este caso? ¿Por qué no tener un objeto ordinario de una clase ordinaria?


Si simplemente desea cambiar el valor, una propiedad no es realmente muy útil, ¿verdad? Simplemente configure el valor del atributo y termine con él.

Una propiedad solo debe usarse si hay algo que ocultar, algo que podría cambiar en una implementación futura.

Tal vez su ejemplo está muy despojado, y hay algunos cálculos infernales que ha dejado. Pero no parece que la propiedad agregue un valor significativo.

Las técnicas de "privacidad" influenciadas por Java (en Python, nombres de atributos que comienzan con _) no son realmente muy útiles. Privado de quien? El punto de privado es un poco nebuloso cuando tienes la fuente (como lo haces en Python).

Los captadores y establecedores de estilo EJB influenciados por Java (a menudo hechos como propiedades en Python) están ahí para facilitar la introspección primitiva de Java, así como para pasar la lista con el compilador de lenguaje estático. Todos esos captadores y establecedores no son tan útiles en Python.

S.Lott
fuente
14
Porque necesito modificar un atributo que de tal manera que sea visto por todas las instancias de una clase, y en el ámbito desde el que se llama a estos métodos de clase no tiene referencias a todas las instancias de la clase.
Mark Roddy