Clases

Python es un lenguaje orientado a objetos, de modo que tiene soporte de primer nivel para la creación de clases. No obstante, no es condición necesaria hacer uso de ellas para poder crear un programa (esto ocurre en otros lenguajes, como Java). De hecho, hemos estado trabajando hasta ahora sin hacer mención a ellas ni emplearlas directamente.

Para seguir este aparado, es importante tener en claro los conceptos básicos de la orientación a objetos. No pretendemos explicarlos aquí pues nos alejaríamos mucho de nuestro objetivo que es una sucinta introducción al lenguaje. Para ello, te invito a que eches un vistazo al artículo sobre Clases y orientación a objetos en Python.

Para crear una clase vamos a emplear la palabra reservada class seguido de un nombre escrito en minúscula, a excepción de la primera letra de cada palabra, que se escribe en mayúscula, y sin guiones bajos.

class Alumno:
    pass

Aquí introducimos, además, la palabra reservada pass, que sirve para rellenar un espacio que es requerido sintácticamente. En efecto, no tiene ninguna función. Por lo que, por el momento, tenemos una clase de nombre Alumno que está vacía.

Sabemos que las clases pueden contener funciones, a las que llamamos métodos. Para ello vamos a usar la misma nomenclatura que aprendimos en el apartado anterior, con la diferencia que esta vez todo nuestro código estará indentado cuatro espacios, para indicar que queremos ubicarlo dentro de la clase.

class Alumno:

    def saludar(self):
        """Imprime un saludo en pantalla."""
        print("¡Hola, mundo!")

Todas las funciones definidas dentro de una clase deberán tener, al menos, un argumento, que por convención se lo llama self y es una referencia a la instancia de la clase. Este concepto se tornará más claro más adelante.

Ahora bien, debemos crear una instancia de nuestra clase.

alumno = Alumno()
alumno.saludar()

¡Perfecto! Nótese que, al invocar a un método de la clase, no debemos indicar el argumento self, ¡Python hace eso por nosotros! De hecho, en nuestro ejemplo, self no es más que el objeto alumno. Aun más, el código anterior es un atajo para el siguiente.

alumno = Alumno()
Alumno.saludar(alumno)

Una clase también puede contener variables, a las que se conoce con el nombre de atributos. Para crear atributos definimos un método especial llamado __init__(), que es invocado por Python automáticamente siempre que se crea una instancia de la clase (conocido también como constructor o inicializador).

class Alumno:

    def __init__(self):
        self.nombre = "Pablo"

    def saludar(self):
        """Imprime un saludo en pantalla."""
        print(f"¡Hola, {self.nombre}!")

¡Momento! Esto se ha complicado un poco ya que hemos introducido algunas nociones que de las que no hemos hablado aún. En primer lugar, definimos el método inicializador ya mencionado que, al igual que el resto, debe llevar por primer argumento a self. Dentro de él creamos una variable llamada nombre. Sin embargo, para que no se pierda una vez finalizada la función y pueda ser accedida desde otros métodos de la clase (incluso por fuera de la clase), la definimos como self.nombre. En rigor, lo que hemos hecho en el inicializador es similar a lo siguiente.

alumno = Alumno()
alumno.nombre = "Pablo"

Recuerda que Python no distingue la creación de la asignación de un objeto, por ende, siempre podemos crear nuevos atributos dándoles un valor.

Por otro lado, modificamos el método saludar() para que emita un saludo personalizado. Como incluimos el valor de un objeto dentro de la cadena (indicando su nombre entre llaves), debemos anteponer una f. Se trata de uno de los tantos métodos para incluir variables dentro de cadenas. Puedes chequear el artículo Formando cadenas de caracteres para profundizar en el tema.

Ahora, volviendo a nuestro código, el resultado es diferente.

alumno = Alumno()
# Imprime ¡Hola, Pablo!
alumno.saludar()

Bien. Sin embargo, claramente todos los alumnos que crearemos no tendrán el mismo nombre. Por ello sería conveniente permitir que, al definir una instancia, se pase como argumento el nombre del alumno y éste se almacene en el atributo self.nombre.

class Alumno:

    def __init__(self, nombre):
        self.nombre = nombre

    def saludar(self):
        """Imprime un saludo en pantalla."""
        print(f"¡Hola, {self.nombre}!")


alumno = Alumno("Pablo")
alumno.saludar()

¿Qué te parece? Los argumentos del método __init__(), a partir del segundo, son requeridos siempre que se quiera crear una instancia de la clase Alumno.

Herencia

La herencia es una herramienta fundamental para la orientación a objetos. Permite definir jerarquías de clases que comparten diversos métodos y atributos. Por ejemplo, consideremos la siguiente clase Rectangulo.

class Rectangulo:
    """
    Define un rectángulo según su base y su altura.
    """
    def __init__(self, b, h):
        self.b = b
        self.h = h

    def area(self):
        return self.b * self.h

rectangulo = Rectangulo(20, 10)
print("Área del rectángulo: ", rectangulo.area())

Hasta aquí, nada nuevo, a excepción del docstring que, como vimos para las funciones, también se aplica a las clases. Supongamos, ahora, que necesitamos definir otra clase Triangulo.

class Triangulo:
    """
    Define un triángulo según su base y su altura.
    """
    def __init__(self, b, h):
        self.b = b
        self.h = h

    def area(self):
        return (self.b * self.h) / 2

En efecto, los códigos son muy similares, a excepción del método area(). Pero dado que el método __init__() es el mismo, podemos abstraerlo en una clase padre de la cual hereden tanto Rectangulo como Triangulo.

class Poligono:
    """
    Define un polígono según su base y su altura.
    """
    def __init__(self, b, h):
        self.b = b
        self.h = h

class Rectangulo(Poligono):

    def area(self):
        return self.b * self.h

class Triangulo(Poligono):

    def area(self):
        return (self.b * self.h) / 2

rectangulo = Rectangulo(20, 10)
triangulo = Triangulo(20, 12)

print("Área del rectángulo: ", rectangulo.area())
print("Área del triángulo:", triangulo.area())

¡Excelente! Nuestro código ha quedado mucho más ordenado, pequeño y conciso.

Una misma clase puede heredar de varias clases (herencia múltiple); en ese caso, se especifican los nombres separados por comas.

class ClaseA(ClaseB, ClaseC, ClaseD):
    pass

El potencial de las clases es muy grande. Baste por el momento con esta pequeña exposición. Nos será útil para avanzar con otras cuestiones del lenguajes.