2010-07-08 15 views
50

Básicamente quiero hacer algo como esto:¿Cómo crear una propiedad de clase de solo lectura en Python?

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

entonces me gustaría trabajar el siguiente:

>>> foo.number 
4 

Por desgracia, lo anterior no funciona. En lugar de darme 4 me da <property object at 0x101786c58>. ¿Hay alguna forma de lograr lo anterior?

Respuesta

36

ElEl descriptorsiempre se devuelve cuando se accede desde una clase (es decir. cuando instance es None en su método __get__).

Si eso no es lo que desea, puede escribir un nuevo descriptor que siempre utiliza la clase de objeto (owner) en lugar de la instancia:

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

Este no es un atributo de solo lectura. Los descriptores de Python intencionalmente no interceptan llamadas de nivel de clase a 'setattr' y' delattr' con sus métodos '__set__' y' __delete__'. Entonces 'Foo.number = 6' podría funcionar. – HardlyKnowEm

47

Esto hará que Foo.number un sólo lectura propiedad:

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 

Explicación: El escenario habitual cuando se utiliza @property se ve así:

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

Una propiedad definido en Foo es de solo lectura con respecto a sus instancias. Es decir, foo.number = 6 elevaría un AttributeError.

Análogamente, si desea Foo.number para elevar un AttributeError, deberá configurar una propiedad definida en type(Foo). De ahí la necesidad de una metaclase.


Tenga en cuenta que esta read-onlyness no es inmune a los piratas informáticos. La propiedad puede ser hecho puede ser escrito por el cambio de la clase de Foo :

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

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

impresiones

6 

o, si desea realizar Foo.number una propiedad configurable,

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) 

impresiones también 6

+0

@unutbu: Es posible realizar el reverso: esto hace que Foo.number se pueda escribir sin reconstruir la clase Foo. – user2284570

+0

@ user2284570: Puede hacer que 'Foo.number' se pueda escribir cambiando la metaclase de' Foo' de 'MetaFoo' a otra cosa. Usted podría hacer que 'number' sea una propiedad setable o simplemente no mencionarlo, en cuyo caso establecer' Foo.number' haría 'Foo.number' un atributo de clase normal. He agregado un código arriba para mostrar lo que quiero decir. – unutbu

+0

@unutbu: ¿Y podría cambiar su metaclase? ¿Funcionaría esto para los objetos de código? – user2284570

12

Estoy de acuerdo con unubtu's answer; parece funcionar, sin embargo, no funciona con esta sintaxis precisa en Python 3 (específicamente, Python 3.4 es con lo que tuve problemas). Así es como uno debe formar el patrón en Python 3.4 para que todo funcione, lo que parece:

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 
2

problema con las soluciones anteriores es que no funcionaría para acceder a las variables de clase de instancia de variable:

print(Foo.number) 
# 4 

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

Por otra parte, el uso de metaclase explícita no es tan agradable, como el decorador regular property.

Traté de resolver este problema. Aquí cómo funciona ahora:

@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 

Como se ve, tenemos @classproperty que funciona igual que @property para las variables de clase. Lo único que necesitaremos es decorador de clase @classproperty_support adicional.

La solución también funciona para las propiedades de clase de solo lectura.

Aquí es puesta en práctica:

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 

Nota: código no se ha probado tanto, no dude en cuenta si no funciona como se espera.

Cuestiones relacionadas