Propiedades controladas

Es posible tomar los valores que el usuario nos pasa al inicializar los objetos (en __init__) y guardarlos en variables privadas. Usamos el guión bajo inicial en la propiedades para indicar que estas son internas de la clase y que no deberián ser usadas. Esto es una convención pero no es obligatorio. Python no lanzará un error si los usuarios de nuestra clase usan estas propiedades privadas.

 1class Persona:
 2    def __init__(self, nombre, apellido):
 3        self._nombre = nombre
 4        self._apellido = apellido
 5
 6    @property
 7    def nombre(self):
 8        return self._nombre
 9
10    @property
11    def apellido(self):
12        return self._apellido

¿Que es @property?

Antes de la definición de una función (esto puede hacerse dentro y fuera de las clases) es posible agregar lo que llamamos un decorador (o función decoradora). La sintaxis para hacerlo es simplemente agregar esta línea sobre la definición de una función y comenzarla con un @. Estos decoradores se usan para modificar a la función de formas que por el momento exceden lo que necesitamos conocer. En particular, @property es usado por las clases en Python para marcar que una propiedad existe y que la funcion decorada sera la encargada de atender las llamadas de lectura de esta propiedad. La escritura/modificación de cada propiedad se hace de otra forma.

Hasta aquí estas propiedades son solo lectura

1victor = Persona('Victor', 'Fernandez')
2print(victor.apellido)
3# funciona y devule
4'Fernandez'
5# La siguiente línea fallará porque no esta todavía definido como funcionará
6# la asignación de esta propiedad
7victor.apellido = 'Gonzalez'

A las funciones de una clase para leer una propiedad se les llama getter y las las funciones para escribir un nuevo valor a una propiedad se las llama setter (por get y set del ingles: obtener y definir).

Formalmente ahora podemos decir que nuestras propiedades nombre y apellido tienen getter pero no setter.

Veamos un ejemplo de setter para la propiedad nombre de la clase Persona:

 1@nombre.setter
 2def nombre(self, value):
 3    # Antes de escribir mi variable privada _nombre, revisar que
 4    # cuampla con los requisitos definidos
 5    if type(value) != str:
 6        # Si no es del tipo *string* lanzaremos (raise) un error
 7        # (excepción) del Tipo Exception (hay otros tipos).
 8        raise Exception('Nombre inválido. Solo string permitido')
 9
10    # solo si pasa las validaciones (podrían ser varias)
11    # sobreescribimos nuestra variable privada con el nuevo valor.
12    self._nombre = value

La función definida para ser setter debe cumplir las siguientes condiciones:

  • Tener un decorador de la forma @NOMBRE_DE_LA_PROPIEDAD.setter.

  • Tener el mismo nombre que la función getter.

  • Incluir un parámetro para recibir el valor que el usuario quiere definir (usualmente lo llamaremos value).

Es posible tambien definir propiedades personalizadas a gusto.

1@property
2def nombre_completo(self):
3    """ devuelve el nombre completo """
4    return f'{self._nombre} {self._apellido}'
5
6@property
7def nombre_formal(self):
8    """ devuelve el nombre completo """
9    return f'{self._apellido}, {self._nombre}'

Estas propiedades solo tienen sentido para ser leidas. Es por esto que no tienen una funcion setter.

Ejemplos de uso:

juan = Persona('juan carlos', 'perez')
print(juan.nombre_completo)
'juan carlos perez'
print(juan.nombre_formal)
'perez, juan carlos'
# Si intento asignar una propiedad que es solo lectura (no tienen una funcion setter)
# dará un error "can't set attribute" (no se puede asignar esta propiedad)
juan.nombre_completo = 'Nuevo nombre completo'

Las propiedades nombre y apellido se pueden leer y escribir. Las propiedades nombre_completo y nombre_formal son simplemente combinaciones útiles de otras propiedades básica. Solo se puede leer.

Funciones de mi clase

Es también posible definir funciones

def limpiar(self):
    """ Mejorar el nombre y el apellido """
    self._nombre = self._nombre.strip().title()
    self._apellido = self._apellido.strip().title()

def encabezado(self, titulo, limpiar=True):
    """ Genera y devuelve el nombre completo con
        "Sr." "Sra." o algun otro titulo.
        Opcionalmente se puede limpiar el nombre """
    # limpiar el nombre si se solicita
    if limpiar:
        self.limpiar()
    return f'{titulo} {self.nombre_completo}'

# podemos tambien emular el comportamiento de los strings
# e incluso copiar nombres de funciones de ellos
def lower(self):
    """ devuelve el nombre completo en minusculas """
    return self.nombre_completo.lower()

def upper(self):
    """ devuelve el nombre completo en minusculas """
    return self.nombre_completo.upper()

Algunos ejemplos de uso con estas nuevas funciones:

juan = Persona('juan carlos', 'perez')
print(juan.nombre_completo)
# 'juan carlos perez'
print(juan.nombre_formal)
# 'perez, juan carlos'

enc = juan.encabezado('Sr.', limpiar=False)
print(enc)
# 'Sr. juan carlos perez'

enc = juan.encabezado('Sr.')
print(enc)
# 'Sr. Juan Carlos Perez'

print(juan.nombre)
# El nombre fue limpiado
# 'Juan Carlos'

print(juan.upper())
# 'JUAN CARLOS PEREZ'

Nota importante: Las funciones se llaman con los parentesis (y parámetros si se requieren) y las propiedades se llaman sin ellos (y no puede requerir parámetros).

Código de la clase final aquí.


class Persona:
    def __init__(self, nombre, apellido):
        self._nombre = nombre
        self._apellido = apellido

    @property
    def nombre(self):
        return self._nombre

    @nombre.setter
    def nombre(self, value):
        if type(value) != str:
            raise Exception('Nombre inválido. Solo string permitido')
        self._nombre = value

    @property
    def apellido(self):
        return self._nombre

    @nombre.setter
    def apellido(self, value):
        if type(value) != str:
            raise Exception('Apellido inválido. Solo string permitido')
        self._nombre = value

    @property
    def nombre_completo(self):
        """ devuelve el nombre completo """
        return f'{self._nombre} {self._apellido}'

    @property
    def nombre_formal(self):
        """ devuelve el nombre completo en modo formal """
        return f'{self._apellido}, {self._nombre}'

    def limpiar(self):
        """ Mejorar el nombre y el apellido """
        self._nombre = self._nombre.strip().title()
        self._apellido = self._apellido.strip().title()

    def encabezado(self, titulo, limpiar=True):
        """ Genera y devuelve el nombre completo con
            "Sr." "Sra." o algun otro titulo.
            Opcionalmente se puede limpiar el nombre """
        # limpiar el nombre si se solicita
        if limpiar:
            self.limpiar()
        return f'{titulo} {self.nombre_completo}'

    # podemos tambien emular el comportamiento de los strings
    # e incluso copiar nombres de funciones de ellos
    def lower(self):
        """ devuelve el nombre completo en minusculas """
        return self.nombre_completo.lower()

    def upper(self):
        """ devuelve el nombre completo en minusculas """
        return self.nombre_completo.upper()

Tareas

  • Hacer un PR a la clase Persona para agregar la propiedad edad.

  • Hacer un PR a la clase Carta para validar que el palo es string en su función setter.

  • Hacer un PR a la clase Carta para validar que el numero es mayor que cero y menor o igual que 12 en su función setter.

Algunos ejemplos de uso

"""
Clase Carta para juegos de cartas
"""

class Carta:
    def __init__(self, numero, palo):
        self._numero = numero
        self._palo = palo

    @property
    def numero(self):
        return self._numero

    @numero.setter
    def numero(self, value):
        if type(value) != int:
            raise Exception('Solo están permitidos numeros')
        self._numero = value

    @property
    def palo(self):
        return self._palo

    @palo.setter
    def palo(self, value):
        self._palo = value

    def __str__(self):
        return f'{self.numero} de {self.palo}'

# Pruebas

carta1 = Carta(3, 'espada')
print(str(carta1))
'3 de espada'