2012-03-29 5 views
24

Tengo muchas clases pequeñas diferentes que tienen algunos campos cada uno, p. esto:¿Cómo debo exponer los campos de solo lectura de las clases de Python?

class Article: 
    def __init__(self, name, available): 
     self.name = name 
     self.available = available 

¿Cuál es la forma más fácil y/o más idiomática para que el campo name de sólo lectura, por lo que

a = Article("Pineapple", True) 
a.name = "Banana" # <-- should not be possible 

ya no es posible?

Esto es lo que yo consideraba hasta ahora:

  1. Utilice un captador (! Uf).

    class Article: 
        def __init__(self, name, available): 
         self._name = name 
         self.available = available 
    
        def name(self): 
         return self._name 
    

    feo, no Pythonic - y una gran cantidad de código repetitivo para escribir (especialmente si tengo varios campos para hacer de sólo lectura). Sin embargo, hace el trabajo y es fácil ver por qué es así.

  2. Uso __setattr__:

    class Article: 
        def __init__(self, name, available): 
         self.name = name 
         self.available = available 
    
        def __setattr__(self, name, value): 
         if name == "name": 
          raise Exception("%s property is read-only" % name) 
         self.__dict__[name] = value 
    

    ve bastante en el lado de la persona que llama, que parece ser la forma idiomática para hacer el trabajo - pero desafortunadamente no tengo muchas clases con sólo unos pocos campos para hacer única de cada lectura. Así que tendría que agregar una implementación __setattr__ a todos ellos. ¿O usar algún tipo de mixin tal vez? En cualquier caso, necesitaría decidir cómo comportarme en caso de que un cliente intente asignar un valor a un campo de solo lectura. Rendimiento cierta excepción, supongo, pero ¿cuál?

  3. Utilice una función de utilidad para definir propiedades (y, opcionalmente, captadores) de forma automática. Esta es básicamente la misma idea que (1), excepto que yo no escribo los captadores de forma explícita, sino más bien hacer algo como

    class Article: 
        def __init__(self, name, available): 
         # This function would somehow give a '_name' field to self 
         # and a 'name()' getter to the 'Article' class object (if 
         # necessary); the getter simply returns self._name 
         defineField(self, "name") 
         self.available = available 
    

    La desventaja de esto es que ni siquiera sé si esto es posible (o cómo implementarlo) ya que no estoy familiarizado con la generación de código de tiempo de ejecución en Python. :-)

Hasta ahora, (2) parece ser más prometedor para mí excepto por el hecho de que voy a necesitar __setattr__ definiciones a todas mis clases. Ojalá hubiera una forma de 'anotar' los campos para que esto ocurra automáticamente. ¿Alguien tiene una mejor idea?

Por lo que vale, estoy cantando Python 2.6.

ACTUALIZACIÓN: Gracias por todas las respuestas interesantes! Por ahora, tengo esto:

def ro_property(o, name, value): 
    setattr(o.__class__, name, property(lambda o: o.__dict__["_" + name])) 
    setattr(o, "_" + name, value) 

class Article(object): 
    def __init__(self, name, available): 
     ro_property(self, "name", name) 
     self.available = available 

Esto parece funcionar muy bien. Los únicos cambios necesarios para la clase original son

  • necesito para heredar object (que no es una cosa tan estúpida de todas formas, supongo)
  • Necesito cambiar self._name = name a ro_property(self, "name", name).

Esto se ve bastante limpio para mí - ¿alguien puede ver un inconveniente con él?

+2

Su 'ro_property' es demasiado complicada: la respuesta de Sven o Chris es la mejor opción. –

+0

@EthanFurman es complicado, pero tal vez ese sea el precio que tiene que pagar si desea mantener los cambios hechos a las clases existentes al mínimo? –

+0

No recuerdo la última vez que escribí sobre el atributo de una instancia (desde fuera de los métodos de la instancia), excepto cuando se usa el código de otra persona. ¿No son los atributos de instancia casi siempre destinados a ser de solo lectura en la práctica? Los atributos de instancia generalmente se establecen en '__init__' y no se modifican después, a menos que se llame a un método en la instancia. Creo que considero este estilo de codificación adecuado. –

Respuesta

37

me gustaría utilizar property como decorador para administrar su captador de name (véase el ejemplo de la clase Parrot en la documentación) El uso, por ejemplo, algo como:

class Article(object): 
    def __init__(self, name, available): 
     self._name = name 
     self.available = available 

    @property 
    def name(self): 
     return self._name 

Si no se define la incubadora para el name propiedad (usando el decorador x.setter alrededor de una función) esto arroja una AttributeError cuando intenta restablecer y name.

Nota: Tienes que utilizar las clases de nuevo estilo de Python (es decir, en Python 2.6 que tiene que heredar de object) para las características funcionen correctamente. Este no es el caso de acuerdo con @SvenMarnach.

+2

Esto se ve bastante por el lado de la persona que llama, pero todavía se ve como la placa de la caldera cuando escribe las clases. Sería hermoso si pudiera hacer algo como '@property self_name = name' más o menos en el constructor para que Python defina el getter automáticamente. Tal como está, este código es más agradable para la persona que llama, pero aún más código para escribir para el autor de la clase que un captador simple (la línea adicional para '@ propiedad'). –

+0

Algo como '@property self._name = name' sería agradable, pero los decoradores trabajan en funciones en lugar de en objetos. Es una línea adicional en su idea getter, pero esta es la forma idiomática de hacerlo, y la forma en que la documentación de Python demuestra la creación de variables de clase de solo lectura. En definitiva, siempre necesitará un código repetitivo para redefinir el comportamiento predeterminado de las variables de clase. – Chris

+1

[Las propiedades también funcionan en clases antiguas.] (Http://ideone.com/wgXAX) El descriptor en sí, es decir, la clase 'property' en sí misma, debe ser una clase de estilo nuevo, pero lo es. La documentación de Python es un poco engañosa a este respecto. –

2

me gustaría seguir con la opción 1, pero refinado que use Python propiedad:

class Article 
    def get_name(self): 
     return self.__name 

    name = property(get_name) 
+4

Los guiones bajos dobles evitan que las subclases anulen accidentalmente los campos. Para los campos "privados" regulares, el subrayado único es correcto. Además, es mejor usar 'property' como decorador. – agf

+1

Hm, suena interesante (especialmente si podría pasar una lambda a 'propiedad' ...), pero parece que no puedo hacer que funcione. 'get_name' sería un método global, y la asignación' name' se haría en el constructor? –

+1

Gracias agf. El código anterior debe colocarse dentro de su clase, no globalmente. – Pinch

4

Cabe señalar que es siempre posible modificar los atributos de un objeto en Python - no hay variables verdaderamente privadas en Python. Es solo que algunos enfoques lo hacen un poco más difícil. Pero un codificador determinado siempre puede buscar y modificar el valor de un atributo. Por ejemplo, siempre puedo modificar su __setattr__ si quiero ...

Para obtener más información, vea Section 9.6 de The Python Tutorial. Python utiliza el cambio de nombre cuando los atributos tienen el prefijo __, por lo que el nombre real en el tiempo de ejecución es diferente, pero aún se puede obtener el nombre en tiempo de ejecución (y, por lo tanto, modificar el atributo).

8

Basado en el Chris respuesta, pero sin duda más Pythonic:

def ro_property(field): 
    return property(lambda self : self.__dict__[field]) 

class Article(object): 
    name = ro_property('_name') 

    def __init__(self): 
     self._name = "banana" 

Si al tratar de modificar la propiedad que elevará una AttributeError.

a = Article() 
print a.name # -> 'banana' 
a.name = 'apple' # -> AttributeError: can't set attribute 

ACTUALIZACIÓN: Sobre su respuesta actualizada, la (pequeña) problema que veo es que está modificando la definición de la propiedad de la clase cada vez que se crea una instancia. Y no creo que sea una buena idea. Es por eso que puse la llamada ro_property fuera de la función __init__

¿Qué pasa ?:

def ro_property(name): 
    def ro_property_decorator(c): 
     setattr(c, name, property(lambda o: o.__dict__["_" + name])) 
     return c 
    return ro_property_decorator 

@ro_property('name') 
@ro_property('other') 
class Article(object): 
    def __init__(self, name): 
     self._name = name 
     self._other = "foo" 

a = Article("banana") 
print a.name # -> 'banana' 
a.name = 'apple' # -> AttributeError: can't set attribute 

decoradores de clase son de lujo!

+0

Me gusta cómo esto reduce la cantidad de cambio que se hará a las clases. –

+0

Muchas gracias por esta respuesta, por favor vea mi respuesta actualizada para lo que ahora tengo. Alcanza justo lo que necesito, pienso, y la chagne para la clase original es solo una línea (en lugar de 'self._prop = prop' ahora hago' ro_property (self, "prop", prop) '). –

+0

Por favor, vea mi respuesta actualizada con una nueva propuesta. – rodrigo

12

Como se señaló en otras respuestas, el uso de una propiedad es el camino a seguir para los atributos de solo lectura. La solución en Chris' answer es la más limpia: utiliza el property() integrado de una manera sencilla y directa. Todos los que estén familiarizados con Python reconocerán este patrón, y no habrá ningún vudú específico para un dominio.

Si no te gusta que cada propiedad necesita tres líneas para definir, aquí es otra forma recta hacia adelante:

from operator import attrgetter 

class Article(object): 
    def __init__(self, name, available): 
     self._name = name 
     self.available = available 
    name = property(attrgetter("_name")) 

En general, no me gusta la definición de las funciones específicas de dominio para hacer algo que puede hacerse fácilmente con herramientas estándar. Leer el código es mucho más fácil si no tienes que acostumbrarte primero a todo lo relacionado con el proyecto.

+2

Tu razonamiento sobre * por qué * la respuesta de Chris es tan buena que realmente me conquistó. Pensé que algún truco ingenioso a lo largo de las líneas de lo que Rodrigo publicó sería realmente bueno, pero de hecho, me dieron algunas cejas levantadas por tanta magia en un solo lugar. –

Cuestiones relacionadas