¿Cómo debo exponer los campos de solo lectura de las clases de Python?

Tengo muchas clases pequeñas diferentes que tienen algunos campos cada una, por ejemplo, esto:

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 de hacer que el campo de name solo lectura, para que

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

ya no es posible?

Esto es lo que he considerado hasta ahora:

  1. Utilice un getter (ugh!).

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

    Feo, no pythonico, y un montón de código repetitivo para escribir (especialmente si tengo varios campos para hacer de solo lectura). Sin embargo, hace el trabajo y es fácil ver por qué es eso.

  2. Utilice __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 

    Parece bonito desde el punto de vista de la persona que llama, parece ser la forma idiomática de hacer el trabajo, pero desafortunadamente tengo muchas clases con solo algunos campos para leer solo cada una. Así que necesitaría agregar una implementación __setattr__ a todos ellos. ¿O usar algún tipo de mezcla tal vez? En cualquier caso, tendría que decidir cómo comportarme en caso de que un cliente intente asignar un valor a un campo de solo lectura. Cede alguna excepción, supongo, pero ¿cuál?

  3. Use una función de utilidad para definir propiedades (y opcionalmente captadores) automáticamente. Esta es básicamente la misma idea que en (1), excepto que no escribo explícitamente a los captadores, sino que hago 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 lo más prometedor para mí, excepto por el hecho de que necesitaré __setattr__ definiciones en todas mis clases. Me gustaría que hubiera una manera de “anotar” los campos para que esto suceda automáticamente. ¿Alguien tiene una idea mejor?

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 bastante bien. Los únicos cambios necesarios para la clase original son

  • Necesito heredar un object (lo cual no es una cosa tan estúpida, supongo)
  • Necesito cambiar self._name = name a ro_property(self, "name", name) .

Esto me parece bastante bueno, ¿alguien puede ver un inconveniente con eso?

Usaría la property como decorador para administrar su captador por name (vea el ejemplo de la clase Parrot en la documentación). Usa, 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 define el definidor para la propiedad de name (usando el decorador x.setter alrededor de una función), esto arroja un AttributeError cuando intenta restablecer el name .

Nota : Debe usar las clases de estilo nuevo de Python (es decir, en Python 2.6 debe heredar de un object ) para que las propiedades funcionen correctamente. Este no es el caso según @SvenMarnach.

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 la respuesta de Chris es la más limpia: utiliza la property() incorporada 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 del dominio.

Si no te gusta que cada propiedad necesite tres líneas para definir, aquí hay otra forma directa:

 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 definir funciones específicas del dominio para hacer algo que se pueda hacer fácilmente con herramientas estándar. Leer código es mucho más fácil si no tiene que acostumbrarse primero a todas las cosas específicas del proyecto.

Basado en la respuesta de Chris, pero posiblemente más python:

 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 se intenta modificar la propiedad, se generará un AttributeError .

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

ACTUALIZACIÓN : Acerca de su respuesta actualizada, el (pequeño) problema que veo es que está modificando la definición de la propiedad en la clase cada vez que crea una instancia. Y no creo que sea una buena idea. Por eso pongo 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 

¡Los decoradores de clase son lujosos!

Se debe tener en cuenta que siempre es 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, consulte la Sección 9.6 del Tutorial de Python. Python utiliza la manipulación de nombres cuando los atributos tienen el prefijo __ por lo que el nombre real en tiempo de ejecución es diferente, pero aún así podría derivar lo que ese nombre en tiempo de ejecución (y por lo tanto modificar el atributo).

Me quedaría con su opción 1 pero la refiné para usar la propiedad de Python:

 class Article def get_name(self): return self.__name name = property(get_name)