Clases y Objetos en Python

En este post voy a explicar las Clases y los Objetos en Python, que son dos conceptos básicos de la programación orientada a objetos, que tenemos que entender muy bien.

Una Clase es un molde definido por el usuario que empleamos para crear objetos. Este molde incluye una serie de propiedades y métodos que son comunes a todos los objetos creados con una clase.

La clase puede ser vista como un molde y el objeto como una implementación concreta sacada de ese molde. Por lo que con una clase podemos generar muchos objetos que tendrán las mismas características.

La programación orientada a objetos basa toda la codificación en objetos. Los objetos intentan replicar objetos reales, para de esta forma poder solucionar problemas que se nos presenten en el mundo real, con nuestros programas. Cuando creamos un objeto a partir de una Clase, decimos que hemos creado una instancia de la Clase.

Python es un lenguaje de programación orientado a objetos, de forma que es posible crear clases a medida a parte de las ya existentes. Una clase tiene una serie de atributos (variables) y métodos (funciones) asociados.

Definir una clase

Cuando instanciemos un objeto mediante una Clase, este se compondrá de parámetros y métodos. Los parámetros son las características del objeto y los métodos vienen a definir el comportamiento.

Para definir una clase empleamos la sentencia class, seguida del nombre que demos a la clase, dos puntos y el código que constituye la clase. En este código, como siempre en Python, irá indexado y en él, definiremos los distintos métodos y parámetros.

La sintaxis básica sería:

class nombre_clase:
       def __init__(self, n):
	instrucciones creación de la clase 
	(aquí asignamos los valores de inicio a los parámetros)
       def nombre_metodo(self):
              instrucciones método

Veámoslo con un ejemplo. Imaginemos que queremos trabajar con planetas, una definición de la clase Planeta podría ser:

# vamos a modelar el objeto planeta
class Planeta:
    def __init__(self,nombre,tamaño,distancia,atmosfera):
 #con init instanciamos un objeto con los valores que se le pasan"""
        self.nombre=nombre
        self.tamaño=tamaño
        self.distancia=distancia
        self.atmosfera=atmosfera

En este ejemplo sólo he definido el constructor, que es el método que sirve para crear un objeto, el método __init__( ).

Este método tiene siempre el nombre init con dos barras bajas antes y después. A continuación, entre paréntesis, se pasan los parámetros. La sintaxis es la siguiente:

def __init__(self, <parámetros> ):

__init__ permite instanciar un objeto de la clase. Es un método obligatorio en la definición.

El parámetro self es obligatorio y debe ir el primero. Con self estamos pasando una referencia a la instancia que estamos creando, y Python lo hace automáticamente, por lo que cuando creemos el objeto, sólo tenemos que pasarle los valores de los parámetros.

Vamos a dotar a la clase de un método, el método visitable que indica si un planeta es visitable o no. Será visitable si tiene atmosfera y la distancia a la tierra es menor de 3000 billones de Km.

La clase Planeta con el método, quedaría:

class Planeta:
    def __init__(self,nombre,tamaño,distancia,atmosfera):
        self.nombre=nombre
        self.tamaño=tamaño
        self.distancia=distancia
        self.atmosfera=atmosfera
    def visitable(self):
        if(self.atmosfera and self.distancia<3000):
            return True
        else:
            return False

Todo método, debe al menos tener el parámetro self para que pueda trabajar con la instancia.

Crear un objeto

Para crear una instancia de la clase, sencillamente utilizamos el nombre de la clase como método al que le pasamos los parámetros requeridos por el método __init__

Cuando creamos un objeto, instancia de la clase, no es necesario pasar el parámetro self, Python lo añade forma automática para pasar la instancia al método, pero en la definición del método es obligatorio incluirlo.

Por ejemplo, una posible instancia de Planeta podría ser planet_1:

planeta_1 = Planeta("Jupiter","Muy grande",3456,True)

En este caso estaríamos pasando los valores de los parámetros por posición, pero también podríamos hacerlo por clave, como en el ejemplo de planet_2:

planeta_2 = Planeta(distancia=324,nombre="Venus",atmosfera=False, tamaño="Pequeñito")

Acceder a los parámetros y métodos de un objeto

Para acceder a los parámetros y métodos de un objeto, sencillamente empleamos el nombre del objeto, seguido de un punto y el parámetro o el método, depende de a que queramos acceder. Si es un parámetro, después del punto se mostrará únicamente el nombre del parámetro. Si es un método, después del punto tendremos el nombre del método con los parámetros que queramos pasar al método. Si no se le pasa ningún parámetro, aparecerán los paréntesis sin nada en su interior.

Ejemplos de llamadas a los parámetros nombre y distancia del objeto planet_1

print("El planeta",planeta_1.nombre,"se encuentra a",planeta_1.distancia,"billones de Km de la tierra")
print(f"El planeta {planeta_1.nombre} se encuentra a {planeta_1.distancia} billones de Km de la tierra")

En el ejemplo anterior podemos ver dos sentencias print que llaman a los parámetros indicados. La segunda empleando una cadena de caracteres formateada. El resultado es el mismo para ambos casos:

El planeta Jupiter se encuentra a 3456 billones de Km de la tierra
El planeta Jupiter se encuentra a 3456 billones de Km de la tierra

Ahora accedemos al método visitable:

if planeta_1.visitable():
    print(f"El planeta {planeta_1.nombre} es visitable ")
else:
    print(f"El planeta {planeta_1.nombre} NO es visitable ")

Puede apreciarse que se trata de un método por los parentesis después del nombre. El resultado sería:

El planeta Jupiter NO es visitable

Podemos programar métodos especificos para actualizar los valores de los parámetros, con idea de tener parte del control. Ejemplo:

def actualizar_distancia(self,valor):
     if (valor<2):
         print("No es posible, un planeta tan cerca de la tierra")
     else:
         self.distancia=valor
         print(f"Registrada distancia del planeta {self.nombre} a la tierra: {self.distancia} billones de Km")

Si llamamos al método:

planeta_1.actualizar_distancia(123)

Obtenemos el resultado:

Registrada distancia del planeta Jupiter a la tierra: 123 billones de Km

El programa completo quedaría:

class Planeta:
    def __init__(self,nombre,tamaño,distancia,atmosfera):
        self.nombre=nombre
        self.tamaño=tamaño
        self.distancia=distancia
        self.atmosfera=atmosfera
    def visitable(self):
        if(self.atmosfera and self.distancia<3000):
            return True
        else:
            return False
    def actualizar_distancia(self,valor):
        if (valor<2):
            print("No es posible, un planeta tan cerca de la tierra")
        else:
            self.distancia=valor
            print(f"Registrada distancia del planeta {self.nombre} a la tierra: {self.distancia} billones de Km")

planeta_1 = Planeta("Jupiter","Muy grande",3456,True)
print("El planeta",planeta_1.nombre,"se encuentra a",planeta_1.distancia,"billones de Km de la tierra")
print(f"El planeta {planeta_1.nombre} se encuentra a {planeta_1.distancia} billones de Km de la tierra")
if planeta_1.visitable():
    print(f"El planeta {planeta_1.nombre} es visitable ")
else:
    print(f"El planeta {planeta_1.nombre} NO es visitable ")
planeta_1.actualizar_distancia(123)

Métodos especiales

Generalmente en una clase definimos una estructura de datos que contiene una serie de métodos para operar con esta estructura. Existen algunos métodos especiales para definir como realizar operaciones comunes con la clase definida. Algunos ejemplos:

__add__: operación de suma. Se define cómo se suman dos objetos de esta clase.  Objeto 1 + Objeto 2. Similares con __sub__, __mul__, __div__, __mod__

__str__: representación orientada al usuario final del objeto

__len__: métodos para definir la longitud de un objeto

__iter__: para implementar funcionalidades de iteración

Por ejemplo, si partimos de la clase Curso:

class Curso:
    def __init__(self, nombre, duracion): 
        self.nombre_curso = nombre
        self.duracion = duracion
    def obtener_descripcion(self):
        print("Curso de " + self.nombre_curso + " con duracion " + str(self.duracion))

E instanciamos dos objetos curso:

curso1 = Curso('Phyton', 20)
curso1.obtener_descripcion() 
curso2 = Curso('SQL', 10)
curso2.obtener_descripcion()

Y ejecutamos el programa. Obtenemos:

Curso de Phyton con duracion 20
Curso de SQL con duracion 10

Hasta aquí todo normal, pero supongamos que queremos definir un método para sumar cursos, en el que se creara un nuevo curso con los nombres de ambos cursos y la duración la suma de las duraciones de ambos cursos. El método podría ser:

def __add__(self, new):
       return Curso(self.nombre_curso + " y " + new.nombre_curso,
                                               self.duracion + new.duracion)

La manera normal de usar el método __add__() sería:

curso1 = Curso('Phyton', 20)
curso2 = Curso('SQL', 10)
curso_total=curso1.__add__(curso2)
curso_total.obtener_descripcion()

Lo que daría el siguiente resultado:

Curso de Phyton y SQL con duracion 30

Ahora bien, como este es un método especial (con las dos barras bajas delante y las dos barras bajas detrás) y representa la suma, podemos llamar al método sencillamente usando el operador suma. La instrucción:

curso_total = curso1 + curso2

Produce el mismo resultado que:

curso_total=curso1.__add__(curso2)

Nuestro programa completo:

class Curso:
    def __init__(self, nombre, duracion): 
        self.nombre_curso = nombre
        self.duracion = duracion
    def obtener_descripcion(self): 
   		print("Curso de " + self.nombre_curso + " con duracion " + str(self.duracion))
    def __add__(self, new):
   		return Curso(self.nombre_curso + " y " + new.nombre_curso, self.duracion + new.duracion)
curso1 = Curso('Phyton', 20)
curso1.obtener_descripcion() 
curso2 = Curso('SQL', 10)
curso2.obtener_descripcion()
curso_total = curso1 + curso2
curso_total.obtener_descripcion()
curso_total=curso1.__add__(curso2)
curso_total.obtener_descripcion()

Genera el resultado:

Curso de Phyton con duracion 20
Curso de SQL con duracion 10
Curso de Phyton y SQL con duracion 30
Curso de Phyton y SQL con duracion 30

Herencia

La herencia es un proceso mediante el cual heredamos una serie de características de nuestros antecesores. Podemos entenderla como un mecanismo para definir una clase nueva a partir de otra ya existente. La clase nueva hereda de la existente sus parámetros y métodos, que no hace falta definirlos de nuevo, sencillamente los hereda. Además, dicha clase nueva puede incluir nuevos parámetros y métodos propios, extendiendo la clase ya existente.

Se denomina clase base o superclass a la clase existente de la que hereda la nueva clase que es conocida como clase hija o derivada.

Por ejemplo, definimos la clase Persona de la siguiente forma:

class Persona:
    def __init__(self, nombre, apellidos, genero):
        self.nombre=nombre
        self.apellidos=apellidos
        self.genero=genero
        
    def nombreCompleto(self):
        nombreCompleto=self.nombre+" "+self.apellidos
        return nombreCompleto

A continuación, definimos la clase empleado, que heredará de la clase Persona. Para indicar la herencia, en la definición de la clase derivada, tras indicar su nombre, entre paréntesis se indica la superclase:

class Empleado(Persona):

Y si definimos esta nueva clase al completo:

class Empleado(Persona):
    def __init__(self, nombre, apellidos, genero, idEmpleado):
        super().__init__(nombre, apellidos, genero)
        self.idEmpleado=idEmpleado
    def identificadorEmp(self):
         print("El empleado ",self.nombreCompleto()," tiene el identificador: ", self.idEmpleado)

Donde puede apreciarse que hemos añadido un parámetro nuevo: idEmpleado, y un método nuevo: identificadorEmp

Recordemos que el método __init__( ) era el método de la clase que empleabamos para instanciar un objeto de la Clase. En el caso de la clase derivada, su método __init__( ) llama al de la superclase: super().__init__() , para instanciar así los parámetros y métodos de la superclase, que hereda la clase derivada.

Cuando creamos una clase derivada, la superclase debe estar definida antes en el código del programa

Ejemplo de creación de objetos que heredan de la clase base:

Persona_1=Persona("José","Martinez","Varon")
print(Persona_1.nombreCompleto())
Empleado_1=Empleado("Ana","Rodriguez","Mujer",57)
print(Empleado_1.nombreCompleto())
Empleado_1.identificadorEmp()

Si ejecutamos el programa, el resultado sería:

José Martinez
Ana Rodriguez
El empleado  Ana Rodriguez  tiene el identificador:  57

Objetos atributos

En ocasiones, cuando definamos una clase, es posible que necesitemos mucho nivel de detalle para uno de los parámetros y nos interese llevárnoslo a otra clase. En la clase original definiríamos un parámetro del tipo de la clase nueva.

Por ejemplo, imaginemos que definimos en la clase Empleado varios parámetros relacionados con el perfil del empleado: nivel_estudios, años_experiencia y posición

Y además definimos un método que trabaje con estos nuevos parámetros, el método salario_estimado

Las clases Persona y Empleado, serían:

class Persona:
    def __init__(self, nombre, apellidos, genero):
        self.nombre=nombre
        self.apellidos=apellidos
        self.genero=genero
        
    def nombreCompleto(self):
        nombreCompleto=self.nombre+" "+self.apellidos
        return nombreCompleto

class Empleado(Persona):
    def __init__(self, nombre, apellidos, genero, idEmpleado, nivel_estudios, años_experiencia, posicion):
        super().__init__(nombre, apellidos, genero)
        self.idEmpleado=idEmpleado
        self.nivel_estudios=nivel_estudios
        self.años_experiencia=años_experiencia
        self.posicion=posicion
    def identificadorEmp(self):
         print("El empleado ",self.nombreCompleto()," tiene el identificador: ", self.idEmpleado)
    def salario_estimado(self):
        if (self.nivel_estudios=="licenciado"):
            complemento=100
        elif (self.nivel_estudios=="bachiller"):
            complemento=50
        else:
            complemento=0
        calculo_salario=self.años_experiencia*(1000+complemento)
        return calculo_salario

Si ahora creamos un objeto empleado y utilizamos el método heredado “nombreCompleto( )”  y el método “salario_estimado”

Empleado_57=Empleado("Ana","Rodriguez","Mujer",57,"bachiller",10,"Directora de Marketing")
print(f"El salario estimado para {Empleado_57.nombreCompleto()} es {Empleado_57.salario_estimado()}")

Si ejecutamos el programa, obtenemos el resultado:

El salario estimado para Ana Rodriguez es 10500

Estos parámetros y el último método que estima el salario tendrían sentido llevárnoslo a otra clase que podríamos llamar Perfil. Esta clase no hereda de ninguna otra, es una clase base. La nueva clase Perfil quedaría:

class Perfil:
    def __init__(self, nivel_estudios, años_experiencia, posicion):
        self.nivel_estudios=nivel_estudios
        self.años_experiencia=años_experiencia
        self.posicion=posicion
    def salario_estimado(self):
        if (self.nivel_estudios=="licenciado"):
            complemento=100
        elif (self.nivel_estudios=="bachiller"):
            complemento=50
        else:
            complemento=0
        calculo_salario=self.años_experiencia*(1000+complemento)
        return calculo_salario

Nos hemos traído a esta nueva clase los parámetros y método de empleado que estaban relacionados con el perfil del Empleado. Es una clase base, independiente, que no hereda de empleado

Y la clase Empleado, actualizada sería:

class Empleado(Persona):
    def __init__(self, nombre, apellidos, genero, idEmpleado,nivel_estudios, años_experiencia, posicion):
        super().__init__(nombre, apellidos, genero)
        self.idEmpleado=idEmpleado
        self.perfil=Perfil(nivel_estudios, años_experiencia, posicion)
    def identificadorEmp(self):
         print("El empleado ",self.nombreCompleto()," tiene el identificador: ", self.idEmpleado)

Puede observarse que hemos definido un parámetro llamado perfil que es un objeto de la clase Perfil. Lo instanciamos en el método __init( )__ de Empleado cuando instanciamos un objeto Empleado. Por ello hay que pasar al método __init( )__ de empleado los valores necesarios para instanciar un objeto Perfil.

Ahora, las llamadas del programa principal serían:

Empleado_57=Empleado("Ana","Rodriguez","Mujer",57,"bachiller",10,"Director MKT")
print(f"El salario estimado para {Empleado_57.nombreCompleto()} es {Empleado_57.perfil.salario_estimado()}")

Puede apreciarse como se accede ahora al método salario_estimado, a través del parámetro perfil que en realidad es una instancia de la clase Perfil

El resultado es el mismo:

El salario estimado para Ana Rodriguez es 10500

Y el programa completo quedaría:

class Perfil:
    def __init__(self, nivel_estudios, años_experiencia, posicion):
        self.nivel_estudios=nivel_estudios
        self.años_experiencia=años_experiencia
        self.posicion=posicion
    def salario_estimado(self):
        if (self.nivel_estudios=="licenciado"):
            complemento=100
        elif (self.nivel_estudios=="bachiller"):
            complemento=50
        else:
            complemento=0
        calculo_salario=self.años_experiencia*(1000+complemento)
        return calculo_salario

class Persona:
    def __init__(self, nombre, apellidos, genero):
        self.nombre=nombre
        self.apellidos=apellidos
        self.genero=genero
        
    def nombreCompleto(self):
        nombreCompleto=self.nombre+" "+self.apellidos
        return nombreCompleto
    
class Empleado(Persona):
    def __init__(self, nombre, apellidos, genero, idEmpleado,nivel_estudios, años_experiencia, posicion):
        super().__init__(nombre, apellidos, genero)
        self.idEmpleado=idEmpleado
        self.perfil=Perfil(nivel_estudios, años_experiencia, posicion)
    def identificadorEmp(self):
         print("El empleado ",self.nombreCompleto()," tiene el identificador: ", self.idEmpleado)

Empleado_57=Empleado("Ana","Rodriguez","Mujer",57,"bachiller",10,"Director MKT")
print(f"El salario estimado para {Empleado_57.nombreCompleto()} es {Empleado_57.perfil.salario_estimado()}")

Módulos

Hemos visto como definir clases en Python. Una vez definida una clase la podemos emplear en el mismo script en la que la hemos definido, para poder emplearla en otros scripts (archivos .py) empleamos los módulos. Un módulo es un archivo .py que contiene definiciones y declaraciones de Python como puede ser funciones o clases, que son susceptibles de ser utilizadas por varios programas, con lo que en lugar de definirlas en todos ellos, las definimos una única vez en un módulo. Para utilizar las funciones definidas en un módulo desde un script este módulo debe ser importado, utilizando lasentencia import

Por ejemplo, si las clases definidas anteriormente: Persona, Empleado y Perfil, prevemos que las vamos a utilizar en varios programas, nos las llevamos a un modulo. Por ejemplo al modulo clases_de_RRHH

De esta forma, mi programa principal quedaría:

import clases_de_RRHH as rrhh

Empleado_57=rrhh.Empleado("Ana","Rodriguez","Mujer",57,"bachiller",10,"Director MKT")
print(f"El salario estimado para {Empleado_57.nombreCompleto()} es {Empleado_57.perfil.salario_estimado()}")

Puede apreciarse que lo primero que hay que hacer es importar el modulo:

import clases_de_RRHH

después, con la instrucción as, defino un alias para hacer referencia al modulo en mi código.

Y finalmente, en el programa principal, cuando tengo que llamar al método Empleado para instanciar el objeto Empleado_57, lo llamo a través del modulo: rrhh.Empleado

El resultado de ejecución es el mismo que obteníamos antes:

El salario estimado para Ana Rodriguez es 10500

NOTA:

Este post es parte de la colección “Python”. Puedes ver el índice de esta colección aquí.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.