2011-03-04 14 views
62

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.¿Cómo hacer una propiedad de la clase?

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 e.i == 20 
e.inc_i() 
assert e.i == 21 

assert Example.I == 10 
Example.inc_I() 
assert Example.I == 11 

¿Es posible la sintaxis que he utilizado anteriormente o necesitaría algo más?

La razón por la que quiero propiedades de clase es para que pueda cargar atributos de clase, lo cual parece lo suficientemente razonable.

+1

No sé Python ... ¿es este el tipo de cosas que estás buscando? http://www.python.org/download/releases/2.2/descrintro/# property –

+0

si estoy leyendo bien, puedes crear métodos para actuar como setter y getter (como en otros idiomas) para que puedas cargar el valor de la propiedad en el primer get ... lo cual creo es lo que querías? –

+0

@ White Dragon. La función de propiedad que está buscando agrega propiedades a instancias de clase, no a las clases mismas. Estoy preguntando sobre 'Example.I' not' e.i'. –

Respuesta

44

Así es como me gustaría hacer 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 colocador no funcionaba en el momento que llamamos Bar.bar, debido a que estamos llamando TypeOfBar.bar.__set__, que no es Bar.bar.__set__.

Añadir una definición metaclase resuelve este:

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 va a estar bien.

+0

hmm .. mi función setter parece ser ignorada, y no puedo obtener nada en __set__ para ejecutar alguna vez (incluso si solo hay algo de basura allí, nada cambia) –

+0

la segunda afirmación aquí falla: http: // drktd. com/6D2b –

+0

Parece que no funciona con setter. – I159

22

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

[actualización]

Vaya, que hace el trabajo:

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 es en Python 2.7. Python 3+ usa una técnica diferente para declarar una metaclase. Use: class MyClass(metaclass=MetaClass):, elimine __metaclass__, y el resto es lo mismo.

+0

Por lo tanto, se puede hacer, pero ¿se puede hacer con un decorador de métodos? –

+0

No conozco ningún decorador que pueda usar directamente en la clase que le interese. Sin embargo, el decorador 'property' en la metaclase debería funcionar ... He editado mi respuesta para incluir un decorado método de metaclase. – dappawit

+1

También vea [esta pregunta similar] (http://stackoverflow.com/questions/128573/using-property-on-classmethods) y sus respuestas. Parece ser similar, por lo que puede tener información más útil :) – Abbafei

1

Si solo necesita la carga diferida, entonces podría simplemente 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 las metaclases parece más limpio y con un comportamiento más predecible.

Tal vez lo que está buscando es el patrón de diseño Singleton. Hay a nice SO QA sobre la implementación del estado compartido en Python.

20

Si define classproperty de la siguiente manera, 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 puede usar esto para propiedades de escritura. Mientras e.I = 20 levantará un AttributeError, Example.I = 20 sobrescribirá el objeto de propiedad en sí.

2

Hasta donde puedo decir, no hay forma de escribir un setter 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 ajustadores que desee.IE, quería una clase con una propiedad title con 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 que la clase real que se desea de forma normal, excepto que tiene usar 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 raro para definir esta metaclase como lo hicimos anteriormente, si vamos a utilizar únicamente en la clase individual. En ese caso, si usa el estilo de Python 2, puede definir la metaclase dentro del cuerpo de la clase. De esta forma no está definido en el alcance del módulo.

10

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

Puede hacer esto con una metaclase ... principalmente. Dappawit es casi obras, pero creo que tiene un defecto:

class MetaFoo(type): 
    @property 
    def thingy(cls): 
     return cls._thingy 

class Foo(object, metaclass=MetaFoo): 
    _thingy = 23 

Esto se consigue un ClassProperty de 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é demonios está pasando aquí? ¿Por qué no puedo acceder a la propiedad de la clase desde una instancia?

Me estaba dando vueltas en esto por bastante tiempo antes de encontrar lo que creo que es la respuesta. @properties Python son un subconjunto de los descriptores, y, a partir de la (el énfasis es mío) descriptor documentation: Comportamiento

El valor por defecto para el acceso atributo es conseguir, establecer o eliminar el atributo del diccionario de un objeto. Por ejemplo, a.x tiene una cadena de búsqueda de comenzando con a.__dict__['x'], entonces type(a).__dict__['x'], y continuando a través de las clases base de type(a)excluyendo metaclases.

Así que el orden de resolución del método no incluye nuestras propiedades de clase (o cualquier otra cosa definida en la metaclase). Es es posible hacer una subclase del decorador de propiedades incorporado que se comporta de manera diferente, pero (cita requerida) Tengo la impresión de que los desarrolladores de Google tenían una buena razón (que no entiendo) para hacerlo camino.

Eso no significa que no tengamos suerte; podemos acceder a las propiedades de la clase en sí muy bien ... y podemos conseguir la clase de type(self) dentro de la instancia, que podemos utilizar para hacer @property despachadores:

class Foo(object, metaclass=MetaFoo): 
    _thingy = 23 

    @property 
    def thingy(self): 
     return type(self).thingy 

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

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

+0

Esta debería ser la respuesta más importante funciona universalmente y también para subclases. – geckon

Cuestiones relacionadas