¿Cómo hacer una propiedad de clase?

En Python puedo agregar un método a una clase con el decorador @classmethod . ¿Hay un decorador similar para agregar una propiedad a una clase? Puedo mostrar mejor de lo que estoy hablando.

 class Example(object): the_I = 10 def __init__( self ): self.an_i = 20 @property def i( self ): return self.an_i def inc_i( self ): self.an_i += 1 # is this even possible? @classproperty def I( cls ): return cls.the_I @classmethod def inc_I( cls ): cls.the_I += 1 e = Example() assert ei == 20 e.inc_i() assert ei == 21 assert Example.I == 10 Example.inc_I() assert Example.I == 11 

¿Es posible la syntax que he usado anteriormente o requeriría algo más?

La razón por la que quiero las propiedades de clase es para poder cargar los atributos de clase de forma perezosa, lo que parece bastante razonable.

Así es como yo haría esto:

 class ClassPropertyDescriptor(object): def __init__(self, fget, fset=None): self.fget = fget self.fset = fset def __get__(self, obj, klass=None): if klass is None: klass = type(obj) return self.fget.__get__(obj, klass)() def __set__(self, obj, value): if not self.fset: raise AttributeError("can't set attribute") type_ = type(obj) return self.fset.__get__(obj, type_)(value) def setter(self, func): if not isinstance(func, (classmethod, staticmethod)): func = classmethod(func) self.fset = func return self def classproperty(func): if not isinstance(func, (classmethod, staticmethod)): func = classmethod(func) return ClassPropertyDescriptor(func) class Bar(object): _bar = 1 @classproperty def bar(cls): return cls._bar @bar.setter def bar(cls, value): cls._bar = value # test instance instantiation foo = Bar() assert foo.bar == 1 baz = Bar() assert baz.bar == 1 # test static variable baz.bar = 5 assert foo.bar == 5 # test setting variable on the class Bar.bar = 50 assert baz.bar == 50 assert foo.bar == 50 

El setter no funcionó en el momento en que llamamos Bar.bar , porque estamos llamando a TypeOfBar.bar.__set__ , que no es Bar.bar.__set__ .

Agregar una definición de metaclase resuelve esto:

 class ClassPropertyMetaClass(type): def __setattr__(self, key, value): if key in self.__dict__: obj = self.__dict__.get(key) if obj and type(obj) is ClassPropertyDescriptor: return obj.__set__(self, value) return super(ClassPropertyMetaClass, self).__setattr__(key, value) # and update class define: # class Bar(object): # __metaclass__ = ClassPropertyMetaClass # _bar = 1 # and update ClassPropertyDescriptor.__set__ # def __set__(self, obj, value): # if not self.fset: # raise AttributeError("can't set attribute") # if inspect.isclass(obj): # type_ = obj # obj = None # else: # type_ = type(obj) # return self.fset.__get__(obj, type_)(value) 

Ahora todo estará bien.

Si define la classproperty siguiente manera, entonces su ejemplo funciona exactamente como lo solicitó.

 class classproperty(object): def __init__(self, f): self.f = f def __get__(self, obj, owner): return self.f(owner) 

La advertencia es que no puedes usar esto para las propiedades de escritura. Mientras eI = 20 generará un AttributeError , Example.I = 20 sobrescribirá el objeto de propiedad en sí.

Creo que puedes hacer esto con la metaclase. Dado que la metaclase puede ser como una clase para la clase (si eso tiene sentido). Sé que puede asignar un __call__() a la metaclase para anular la llamada a la clase, MyClass() . Me pregunto si el uso del decorador de property en la metaclase funciona de manera similar. (No he probado esto antes, pero ahora tengo curiosidad …)

[actualizar:]

Wow, funciona:

 class MetaClass(type): def getfoo(self): return self._foo foo = property(getfoo) @property def bar(self): return self._bar class MyClass(object): __metaclass__ = MetaClass _foo = 'abc' _bar = 'def' print MyClass.foo print MyClass.bar 

Nota: Esto está en Python 2.7. Python 3+ utiliza una técnica diferente para declarar una metaclase. Use: class MyClass(metaclass=MetaClass): elimine __metaclass__ , y el rest es el mismo.

[respuesta escrita basada en python 3.4; la syntax de metaclase difiere en 2, pero creo que la técnica seguirá funcionando]

Puedes hacer esto con una metaclase … en su mayoría. Dappawit casi funciona, pero creo que tiene un defecto:

 class MetaFoo(type): @property def thingy(cls): return cls._thingy class Foo(object, metaclass=MetaFoo): _thingy = 23 

Esto te da una propiedad de clase en Foo, pero hay un problema …

 print("Foo.thingy is {}".format(Foo.thingy)) # Foo.thingy is 23 # Yay, the classmethod-property is working as intended! foo = Foo() if hasattr(foo, "thingy"): print("Foo().thingy is {}".format(foo.thingy)) else: print("Foo instance has no attribute 'thingy'") # Foo instance has no attribute 'thingy' # Wha....? 

¿Qué diablos está pasando aquí? ¿Por qué no puedo llegar a la propiedad de clase desde una instancia?

Me golpeé la cabeza en esto durante bastante tiempo antes de encontrar lo que creo que es la respuesta. Python @properties es un subconjunto de descriptores y, a partir de la documentación del descriptor (el énfasis es mío):

El comportamiento predeterminado para el acceso a atributos es obtener, configurar o eliminar el atributo del diccionario de un objeto. Por ejemplo, ax tiene una cadena de búsqueda que comienza con a.__dict__['x'] , luego type(a).__dict__['x'] , y continúa a través de las clases base de type(a) excluyendo metaclases .

Por lo tanto, el orden de resolución del método no incluye nuestras propiedades de clase (o cualquier otra cosa definida en la metaclase). Es posible crear una subclase del decorador de propiedades incorporado que se comporte de manera diferente, pero (cita requerida) tengo la impresión de que los desarrolladores tienen una buena razón (que no entiendo) para hacerlo de esa manera.

Eso no significa que estamos fuera de suerte; podemos acceder a las propiedades de la clase en sí muy bien … y podemos obtener la clase de type(self) dentro de la instancia, que podemos usar para hacer que los despachadores de @property:

 class Foo(object, metaclass=MetaFoo): _thingy = 23 @property def thingy(self): return type(self).thingy 

¡Ahora Foo().thingy funciona según lo previsto tanto para la clase como para las instancias! También continuará haciendo lo correcto si una clase derivada reemplaza su _thingy subyacente (que es el caso de uso que originalmente me puso en esta búsqueda).

Esto no es 100% satisfactorio para mí, tener que hacer la configuración tanto en la clase de metaclase como en la clase de objeto parece que viola el principio DRY. Pero este último es solo un despachador de una línea; En general, estoy de acuerdo con que ya exista, y probablemente podrías compactarlo en una lambda o algo así si realmente quisieras.

Por lo que puedo decir, no hay forma de escribir un configurador para una propiedad de clase sin crear una nueva metaclase.

He encontrado que el siguiente método funciona. Defina una metaclase con todas las propiedades de clase y los configuradores que desee. IE, quería una clase con una propiedad de title con un setter. Esto es lo que escribí:

 class TitleMeta(type): @property def title(self): return getattr(self, '_title', 'Default Title') @title.setter def title(self, title): self._title = title # Do whatever else you want when the title is set... 

Ahora haga la clase real que desea normalmente, excepto que use la metaclase que creó anteriormente.

 # Python 2 style: class ClassWithTitle(object): __metaclass__ = TitleMeta # The rest of your class definition... # Python 3 style: class ClassWithTitle(object, metaclass = TitleMeta): # Your class definition... 

Es un poco extraño definir esta metaclase como hicimos anteriormente si solo la usaremos en una sola clase. En ese caso, si está utilizando el estilo Python 2, puede definir la metaclase dentro del cuerpo de la clase. De esa manera no está definido en el scope del módulo.

Si solo necesita una carga diferida, podría tener un método de inicialización de clase.

 EXAMPLE_SET = False class Example(object): @classmethod def initclass(cls): global EXAMPLE_SET if EXAMPLE_SET: return cls.the_I = 'ok' EXAMPLE_SET = True def __init__( self ): Example.initclass() self.an_i = 20 try: print Example.the_I except AttributeError: print 'ok class not "loaded"' foo = Example() print foo.the_I print Example.the_I 

Pero el enfoque de metaclase parece más limpio y con un comportamiento más predecible.

Quizás lo que estás buscando es el patrón de diseño de Singleton . Hay un buen SO QA sobre la implementación del estado compartido en Python.

Se me ocurrió una solución muy similar a @Andrew, solo DRY

 class MetaFoo(type): def __new__(mc1, name, bases, nmspc): nmspc.update({'thingy': MetaFoo.thingy}) return super(MetaFoo, mc1).__new__(mc1, name, bases, nmspc) @property def thingy(cls): if not inspect.isclass(cls): cls = type(cls) return cls._thingy @thingy.setter def thingy(cls, value): if not inspect.isclass(cls): cls = type(cls) cls._thingy = value class Foo(metaclass=MetaFoo): _thingy = 23 class Bar(Foo) _thingy = 12 

Esto tiene la mejor de todas las respuestas:

La “metapropiedad” se agrega a la clase, de modo que seguirá siendo una propiedad de la instancia.

  1. No es necesario redefinir la cosa en ninguna de las clases.
  2. La propiedad funciona como una “propiedad de clase” tanto para la instancia como para la clase
  3. Tiene la flexibilidad de personalizar cómo se hereda _thingy

En mi caso, en realidad personalicé _thingy para ser diferente para cada niño, sin definirlo en cada clase (y sin un valor predeterminado) por:

  def __new__(mc1, name, bases, nmspc): nmspc.update({'thingy': MetaFoo.services, '_thingy': None}) return super(MetaFoo, mc1).__new__(mc1, name, bases, nmspc)