jueves, 14 de octubre de 2010

Las propiedades (Property) de Python

Cuando estamos trabajando con clases, en Python, lo normal es crear unos atributos y unos métodos que accedan a dichos atributos, para modificar, asignar o devolver su valor. Para ello se aconseja que los atributos sean ocultos, de manera que únicamente, a través de los métodos, se pueda acceder a sus valores. Por ejemplo, una clase que defina a una persona puede ser la siguiente:

class ser_humano(object):
def __init__(self, edad, altura, peso):
self.__edad=edad
self.__altura=altura
self.__peso=peso

def getPeso(self):
return self.__peso

def setPeso(self, peso = None):
self.__peso = peso

def getAltura(self):
return self.__altura

def setAltura(self, altura = None):
self.__altura = altura

def getEdad(self):
return self.__edad

def setEdad(self, edad = None):
self.__edad = edad


Como se puede observar al instanciar la clase se tienen que pasar los valores de los atributos para la inicialización. Para modificar los valores de los mismos se ha implementado, para cada atributo un método del tipo set****, y para devolver el valor del atributo se implementa el get****. Podemos ver en nuestro shell favorito al instanciar la clase, de lo que disponemos en el objeto creado:

persona = ser_humano(32,1.70,70)


En Python hay otra alternativa al uso de set**** ó get**** y es envolver dichos métodos dentro de propiedades.

Una propiedad (property) en Python es un mecanismo que se utiliza para dar propiedades
a la instanciación de clases (a los objetos). Las propiedades realizan tareas parecidas a las que realizan __getattr__, __setattr__ y __delattr__, pero más rápido y fácil. Una propiedad se crea llamando a property y vinculando su resultado al atributo de clase. La sintaxis es la siguiente:

atributo = property(fget = None, fset = None, fdel = None, doc = None)

Veamos la creación de propiedades en la clase anterior:

class ser_humano(object):
def __init__(self, edad, altura, peso):
self.__edad=edad
self.__altura=altura
self.__peso=peso

def __getPeso(self):
return self.__peso

def __setPeso(self, peso = None):
self.__peso = peso

def __getAltura(self):
return self.__altura

def __setAltura(self, altura = None):
self.__altura = altura

def __getEdad(self):
return self.__edad

def __setEdad(self, edad = None):
self.__edad = edad

peso = property(fget = __getPeso, fset = __setPeso, doc = 'Peso')
edad = property(fget = __getEdad, fset = __setEdad, doc= 'Edad')
altura = property(fget = __getAltura, fset = __setAltura, doc = 'Altura')


Lo que ahora hemos hecho ha sido ocultar todos los métodos get**** y set**** y crear 3 propiedades nuevas, de manera que sólo ellas sean públicas. Nos vamos al shell y vemos que únicamente aparece como interfaz pública del objeto creado peso, edad y altura.

Haciendo:
a = ser_humano(29,1.65,98)

a es una instancia de ser_humano. Cuando se hace referencia a a.atributo, Python llama en a al método que se declara como argumento fget. Así, print a.altura devolverá 1.65. Cuando asignamos a.atributo = valor, Python llama al método que definimos como argumento fset, esto es, a fset se le pasa valor. Podemos modificar el peso haciendo a.peso = 70. Para finalizar, cuando ejecutamos del x.atributo, Python llama al método que declaramos como argumento en fdel. Python utiliza el argumento doc como cadena de documentación del atributo.

Cuando un argumento no se declara la operación correspondiente se impide. Así, por ejemplo, si queremos que el atributo edad sea de sólo lectura tendremos que declarar la propiedad tal que así: edad = property(fget = __getEdad, doc= 'Edad') Es decir, hacemos que el atributo edad sea de solo lectura porque definimos el argumento fget y no definimos fset ni fdel. Si quisiéramos dar un valor a la propiedad edad daría error (AttributeError: can't set attribute), como cabría esperar:

Mediante las propiedades podemos hacer públicos los atributos de forma completamente segura, como parte de la interfaz pública de la clase.

Cuidado con la herencia

Las propiedades se heredan como cualquier otro atributo. Sin embargo los métodos que se utilizan en las propiedades son los que se definen en la clase en la que la propiedad se define. Veámoslo con el siguiente ejemplo:

class numero_pi(object):
def pi(self):
return 3.14
pi_magico = property(pi)

class numero_pi_mejorado(numero_pi):
def pi(self):
return 3.14159

a = numero_pi_mejorado()
print a.pi_magico

El lector esperaría 3.14159, pero en realidad es 3.14, ya que la propiedad hace referencia a los métodos de la clase en la que está definida ella misma. Para solventar el problema podemos envolver el método pi en otro método, tal que así:

class numero_pi(object):
def pi(self):
return 3.14
def pi_envuelto(self):
return self.pi()
pi_magico = property(pi_envuelto)

class numero_pi_mejorado(numero_pi):
def pi(self):
return 3.14159

a = numero_pi_mejorado()
print a.pi_magico


Saludos.

8 comentarios:

  1. otra cosa que se suele hacer para evitar dicho problema es:

    pi_magico = property(lambda self:self.pi())

    saludos!

    ResponderEliminar
  2. Hola navegante anónimo! Muchas gracias por la información, original y pythonica. Saludos!!!

    ResponderEliminar
  3. Estoy aprendiendo Python y siguiendo un manual me tope con property, el cual, dicho manual no me explicaba que era property, asi que buscando en google, me he topado con este blog que desconocia y en donde explicas claramente que es y como trabaja property.

    Muchas gracias, gracias a ti me ha quedado claro.
    Saludos ;-)

    ResponderEliminar
  4. Muchas gracias. También me ha resultado útil. Estoy haciendo una aplicación donde he declarado así los getters y setters y me has ahorrado trabajo!

    ResponderEliminar
  5. cool!! pero las propiedades ahora son decoradores

    @propersty

    ResponderEliminar
  6. Genial la info, la encontré porque en vim escribí property y me genero toda la estructura solito (groso el abuelo) y como no sabia que era googleando me encontré con tu blog donde lo explicaste muy bien, te dejo la estructura de lo que me genero vim ya que estudiándola un poco me parece mas sencilla:

    def operation_mode():
    doc = "Retorna o establece el modo de operacion."
    def fget(self):
    return self._operation_mode
    def fset(self, value):
    self._operation_mode = value
    def fdel(self):
    del self._operation_mode
    return locals()
    operation_mode = property(**operation_mode())


    ResponderEliminar
  7. Hola, si un atributo es de tipo Objeto (uno previamente definido), ¿se puede establecer una propiedad (property) para ese atributo que contiene un objeto?
    Muchas gracias.

    ResponderEliminar