Programación orientada a objetos

Python es un lenguaje como ya mencionamos anteriormente orientado a objetos. Los objetos contienen una serie de métodos y propiedades que lo definen. Si pensamos en un humano, tenemos como propiedades el color de ojos, color de piel, altura… y los métodos serían las acciones como el baile, leer, saltar, correr…

Esta metodología de programación nos permite reusar el código las veces que queramos sin tener que estar escribiendo múltiples líneas con funciones específicas para determinadas partes de nuestro código si lo hicieramos en una programacioń estructurada o procedural.

Principios básicos

En Python tenemos los siguientes principios:

  • Clase: Tipo de dato que contiene un esquema de métodos y propiedades que se usarán para construir un objeto.

  • Objeto: Una clase que se ha inicializado, es decir, existe y tiene nombres y apellidos propios en su ejecucioń.

  • Herencia: Propiedades o métodos que ha recibido una clase hija por parte de una clase padre, esto no quiere decir que vaya a heredar todos los métodos y propiedades, solo aquellos que se le permitan.

  • Encapsulación: La habilidad de enseñar aquello que solo se pueda mostrar y esconder lo que no nos interesa visibilizar.

  • Polimorfismo: La capacidad que tiene un objeto para cambiar su rol al mismo tiempo, puede actuar de un rol y en otro rol al mismo tiempo.

¿Qué es una clase?

Una clase es la plantilla que tendrá el objeto que queremos crear, contiene una serie de méotodos y propiedades que los utilizaremos una vez generemos el objeto. Por supuesto, una clase es un tipo de dato dentro de Python.

¿Cómo declaramos una clase?

Para generar una clase seguiremos esta sintaxis:

class NombreClase:
  # Bloque de código

¿Cómo podemos añadirle propiedades y métodos a una clase?

De la siguiente forma:

class Coche:
  propiedad = valor

  def método(self):
    # Bloque de código

Un ejemplo podría ser:

class Coche:
  marca = "Toyota"
  modelo = "Corolla"

  def publicidad(self):
    print("De 0 a 100 en 10 segundos")

Tenemos la clase Coche, con unas propiedades definidas que son la marca y el modelo, además, tenemos un método llamado publicidad que muestra un mensaje. En el siguiente apartado, veremos como trabajar con esta clase pero desde la vista de un objeto.

¿Qué es un objeto?

Un objeto es la materialización de una clase, es decir, cuando lo generamos a partir de unas instrucciones ya empieza a existir en nuestro programa que estemos desarrollando. Por ejemplo, tenemos la clase Casa, evidentemente, las casas que declaremos tiene propiedades y métodos diferentes, por ejemplo, tiene una dirección, un número, una elevación, una función, un espacio diferente. Si creamos 5 casas, hemos creado 5 objetos partiendo de una sola clase que es la clase Casa.

En resumen:

  • Es la unidad básica de POO

  • Representa una instancia particular partiendo de una clase

  • Puede haber más instancias partiendo de una misma clase

  • Cada objeto puede contener y mantener su información

¿Cómo declaramos un objeto?

La sintaxis es:

NombreObjeto = NombreClase()

En este ejemplo creamos 3 objetos diferentes partiendo de la clase anterior.

NombreObjeto = NombreClase()
NombreObjeto2 = NombreClase()
NombreObjeto3 = NombreClase()

Para acceder a sus propiedades

NombreObjeto.propiedad1

Para acceder a sus métodos

NombreObjeto.método1()

Utilizando el ejemplo que hemos creado antes:

 1class Coche:
 2  marca = "Toyota"
 3  modelo = "Corolla"
 4  publicidad = "De 0 a 100 en 10 segundos"
 5
 6  def eslogan(self):
 7    print("Este es un método",self.publicidad)
 8
 9Sara = Coche()
10Ionela = Coche()

Tenemos a dos personas que utilizan el mismo coche Ionela y Sara y lo vemos:

1print(Ionela.marca,Ionela.modelo)
2Toyota Corolla
3Ionela.eslogan()
4De 0 a 100 en 10 segundos
5
6print(Sara.marca,Sara.modelo)
7Toyota Corolla
8Sara.eslogan()
9De 0 a 100 en 10 segundos

¿Qué pasa si Sara quiere cambiar de coche?

1Sara.marca = "Citröen"
2Sara.modelo = "Xsara"
3Sara.publicidad = "El confort no es discutible."
4print(Sara.marca,Sara.modelo)
5Citröen Xsara
6Sara.eslogan()
7El confort no es discutible.

¿Pero Ionela ha cambiado de coche?

1print(Ionela.marca,Ionela.modelo)
2Toyota Corolla
3Ionela.eslogan()
4De 0 a 100 en 10 segundos

¡Ya lo tenemos! Podemos instanciar objetos diferentes partiendo de una misma clase y cambiar sus propiedades sin afectar al resto de objetos. ¿A qué es sencillo? Pero, ¿Qué pasa si queremos cualquier persona pueda tener un coche diferente desde el principio?

Pues a pesar de que se puede hacer creando un método que le asigne el valor a las propiedades, lo más correcto es utilizando el método __init__. Este método constructor (que permite inicializar un objeto) asigna valores a las propiedades del objeto cuando se construye, por ejemplo.

1class NombreClase:
2  def __init__(self, valor_propiedad1, valor_propiedad2):
3    self.propiedad1 = valor_propiedad1
4    self.propiedad2 = valor_propiedad2
5
6  def metodo1(self):
7    # Bloque de código

Para crear el objeto:

Variable=NombreClase("Valor de ejemplo n1","Valor de ejemplo n2")

Si parece dificultoso de entender, no pasa nada, este es otro ejemplo con comentarios (¡Será por ejemplos!):

 1# Definimos la clase Coche
 2class Coche:
 3
 4  # Definimos el constructor que sustituirá los
 5  # valores de las propiedades cuando las definamos al inicializar el objeto.
 6
 7  def __init__(self,marca,modelo, velocidad):
 8    self.marca = marca
 9    self.modelo = modelo
10    self.velocidad = velocidad
11
12  # Cuando se llama a este método la velocidad se incrementa +1.
13  def acelerar(self):
14    self.velocidad += 1
15    print(self.velocidad)
16
17  # Cuando se llama a este método la velocidad disminuye en -1.
18  def frenar(self):
19    self.velocidad -= 1
20    print(self.velocidad)
21
22  # Creamos los objetos:
23
24  Chami = Coche("Nissan", "Almera", 0)
25  Jose = Coche("Toyota","Corolla AE92 GTi Twin Cam", 0)
26
27  # Imprimimos los valores que tienen las propiedades de cada objeto:
28
29  print("El coche de Chami es un:",Chami.marca,Chami.modelo,"y ahora va a",Chami.velocidad,"km/h.")
30  print("El coche de Jose es un:",Jose.marca,Jose.modelo,"y ahora va a",Jose.velocidad,"km/h.")
31
32  # Aumentamos la velocidad a uno de los objetos:
33  Chami.acelerar()
34
35  # Si queremos aumentar más veces la velocidad, podemos usar un bucle
36  for x in range(0, 100):
37    Chami.acelerar()
38
39  # Imprimimos la velocidad actual
40  print("Chami va a una velocidad de %i" % Chami.velocidad)

Este ejemplo si lo ejecutamos dará como resultado:

El coche de Chami es un: Nissan Almaera y ahora va a 0 km/h.
El coche de Jose es un: Toyota Corolla AE92 GTi Twin Cam y ahora va a 0 km/h.

Herencia

Una de las propiedades que mencionamos que podían tener los objetos es la herencia, por lo que una clase hija puede contener propiedades y métodos de una clase padre. Veamos un ejemplo:

 1# Definimos la clase padre:
 2class ClasePadre:
 3# Decimos que se ejecute el código sin hacer nada.
 4  pass
 5
 6# Y aquí la clase hija:
 7class ClaseHija(ClasePadre):
 8  pass
 9
10# Creamos el objeto
11Objeto = ClaseHija()

Este es un ejemplo:

 1# Definimos la clase Familia
 2class Familia():
 3
 4  # Con sus propiedades que se rellenarán cuando se inicialice el objeto.
 5  def __init__(self, miembros, apellidos):
 6    self.miembros = miembros
 7    self.apellidos = apellidos
 8
 9    # Cuando se cree la clase, mostrará el apellido que ha recibido.
10    print("Apellidos: %s" % (self.apellidos))
11
12# Creamos la clase Hijo que hereda de Familia
13class Hijo(Familia):
14
15  # Se rellenarán propiedades para este objeto.
16  def __init__(self, nombre, apellidos):
17
18  # Si queremos heredar propiedades y métodos, tendremos que hacer uso de la función super()
19  # super() lo explicaremos más adelante.
20  # Aquí llamamos la propiedad específica de Familia, Familia.apellidos y la inicializamos
21    super().__init__(self,apellidos)
22
23  # Definimos aquí los valores que tendrán estas propiedades
24    self.nombre = nombre
25    self.apellidos = apellidos
26
27# Añadimos un método para el Hijo
28  def mostrar_info(self):
29
30    # Decimos que imprima self.nombre y self.apellidos.
31    print("Soy %s y soy de la familia %s" % (self.nombre,self.apellidos))
32
33# Creamos el objeto
34Pugsley = Hijo("Pugsley","Adams")
35
36# Llamamos al método mostrar_info()
37Pugsley.mostrar_info()

Seguro que te preguntas sobre super().__init__(...), esta función como comentamos permite heredar propiedades y métodos de otra clase. Vendría a ser lo mismo que:

 1class A:
 2  def __init__(self, ejemplo):
 3    self.ejemplo = ejemplo
 4
 5class B(A):
 6  def __init__(self, x, y, z):
 7
 8    # Este procedimiento es más complicado y más tedioso de hacer.
 9    self.guardar_info = A(x)
10
11obj = B(2,3,4)
12print(obj.guardar_info.ejemplo)

Sobreescribiendo métodos en clases hijas

Se puede hacer evidentemente, si en la clase A tenemos un método llamado saludar(), y la clase B que hereda de la clase A, le podemos definir el contenido del mensaje que devolverá el método saludar().

 1class A:
 2  def saludar(self):
 3    print("Hola mundo")
 4
 5class B(A):
 6  def saludar(self):
 7    print("Hello everybody")
 8
 9obj = B
10obj.saludar()

Y devolverá Hello everybody.

Tipos de herencia

Bien, habiendo visto un ejemplo de herencia, también os cuento, que hay distintos ejemplos de herencia:

  • Simple

  • Múltiple

  • Multi nivel

  • Jerárquica

  • Híbrida

Simple

Es el tipo de herencia que hemos visto hasta ahora.

class A:
  pass
class B(A):
  pass

Múltiple

Es una clase que hereda desde otras clases, por lo que tendrá propiedades y métodos de ambas clases (A y B).

 1class A:
 2  pass
 3class B:
 4  pass
 5class C(A,B):
 6  pass
 7
 8# Establecemos una comparación para ver si realmente son subclases o no.
 9# Devolverá True o False dependiendo de si es correcto o no.
10issubclass(C,A) and issubclass(C,B)

Multinivel

Esto se refiere, a que tenemos una clase abuelo, de la cuál hereda una clase padre, del cuál hereda una clase hijo.

class A:
  pass
class B(A):
  pass
class C(B):
  pass

Como vemos, la clase C hereda de la clase B, la clase B de la clase A, y A es la clase principial de primer nivel. Por lo tanto, la clase C herederá propiedades y métodos de todas sus clases superiores a menos que se establezca qué propiedades o métodos se podrán heredar, esto forma parte del encapsulamiento que veremos más tarde.

Jerárquica

Tenemos múltiples clases que heredan de una sola clase, es decir.

1class A:
2  pass
3class B(A):
4  pass
5class C(A):
6  pass
7class D(A):
8  pass

Un ejemplo puede ser, clase Jefe/Jefa de una empresa que tiene el rol más alto de una organización y que por debajo de ellos hay otros roles acordes a la labor de la empresa que tienen menos privilegios, otras funciones…etc

Híbrido

Es la combinación de una o múltiples clases con una o múltiples clases por ejemplo: Imaginamos que tenemos 5 clases (A,B,C,D,E).

  • Clase A es una clase padre.

  • Clase B,C,D heredan de la clase A

  • Clase E, hereda de la clase B y D.

  • Clase E es la clase padre de B y D.

Aquí podemos identificar varios tipos de herencia:

  • A, B, C, D, C = Herencia híbrida

  • B, C, D que heredan de A = Herencia jerárquica

  • E que hereda de B y D = Herencia múltiple

  • C hereda de A = Herencia simple

Un ejemplo de sintaxis:

class A:
  pass
class B(A):
  pass
class C(A):
  pass
class D(A):
  pass
class E(B,D)

Si añadimos una variable en la clase A, creamos un objeto que referencie a E:

 1class A:
 2  hello_world = "Hola Mundo"
 3class B(A):
 4  pass
 5class C(A):
 6  pass
 7class D(A):
 8  pass
 9class E(B,D):
10  pass
11obj = E()
12  print(obj.hello_world)

obj habrá impreso "Hola Mundo".

Función super()

Se utiliza para llamar a métodos de una clase padre, hemos visto en un ejemplo anterior como llamábamos a super().__init__(self, nombre, apellidos) en el ejemplo de la Familia Adams. Aquí estábamos llamando al método inicializador de la clase Familia. Pero podemos llamar a otros métodos también. super().método().

class Vehiculo:
  def arrancar(self):
    print("Arrancamos el coche")
  def parar(self):
    print("Paramos el coche")

class Conductor(Vehiculo):
  def soplar(self):
    print("Soplando, soplando y soplando...")

  def control_policia(self):
    super().parar()
    print("Persona - Hola agente, buenos días")
    print("Policía - Hola, vamos hacerle una prueba de alcoholemia, por favor, sople en la boquilla")
    print("Persona - Vale")
    self.soplar()
    print("Policía - Genial, puede usted proseguir")
    super().arrancar()

Antonio = Conductor()
Antonio.control_policia()

Como vemos, no hace falta que llamemos a __init__ porque __no estamos inicializando ningún valor en ninguna propiedad __ y como se ejecutan los métodos parar() y arrancar() que forman parte de la clase Vehiculo.

Encapsulamiento

Encapsular permite abstraer cierta información al mundo y mostrar solo aquella que interese. Por ejemplo, cuando enviamos un paquete por correos, el personal de correos no puede ver el contenido del paquete, pero si que puede ver el destinatario y el remitente, pudiendo identificar a las dos personas implicadas y saber sus direcciones de correo postal.

¿Cómo encapsular?

Para encapsular, básicamente tendremos que añadirle dos guiones bajos "_ __ _ " delante de la propiedad que queremos ocultar.

class A:
  self._propiedad = valor

Veamos un ejemplo:

class Persona:
  def __init__(self):
    self.nombre  = "Susana"
    self.__apellidos = "Bramura"
    self._tlfno = "777 777 777"

Carlos = Persona()

print(Carlos.apellidos)

Veremos un error parecido a este:

Traceback (most recent call last):
  File "main.py", line 9, in <module>
    print(Carlos.apellidos)
AttributeError: 'Persona' object has no attribute 'apellidos'

Y nos preguntaremos… Pero, si llamamos a Carlos.apellidos y nosotros hemos puesto: Carlos.__apellidos, ¿no sería más correcto para querer obtener el valor de los apellidos de la clase Persona? Bueno, aunque pensemos esto, si utilizamos __ igualmente dará el mismo error porque no se puede acceder desde fuera a una propiedad o método encapsulado.

print(Carlos.__apellidos)
Traceback (most recent call last):
  File "main.py", line 8, in <module>
    print(Carlos.__apellidos)
AttributeError: 'Persona' object has no attribute '__apellidos'

¿Cómo podemos acceder o modificar las variables, propiedades, o los métodos de ámbito privado?

Tendremos que crear métodos específicos que puedan acceder a esas variables, propiedades o métodos.

 1class A:
 2  # Constructor de clase
 3  def __init__(self):
 4
 5    # Asignamos 20 a esta propiedad privada
 6    self.__propiedad = 20
 7
 8  def __metodo(self):
 9    print("Soy un método privado.")
10
11  # Soy un método público que leerá y ejecutará contenido privado.
12  def mostrar(self):
13    self.__metodo()
14    print(self.__variable)
15
16   def cambiar(self,propiedad):
17     self.__propiedad = propiedad
18     print("Esta es la nueva propiedad %i % (self.__propiedad))
19
20# Instanciamos el objeto
21obj = A()
22
23# Ejecutamos el método público mostrar()
24obj.mostrar()
25
26# Modificamos el valor propiedad
27obj.cambiar(300)

Polimorfismo

Es la capacidad que tiene un objeto para ser y poder ser otra cosa al mismo tiempo. Por ejemplo, un pájaro. Un pájaro puede ser un pingüino y un gorrión, ambos tienen propiedades en común como las patas, ojos, orejas; el color de las plumas, de los ojos, de los picos. También, tienen una serie de métodos similares como volar, poner huevos, comer, dormir… ¿Qué tienen en común todos ellos? Que son pájaros. Por lo tanto, un pájaro puede ser un gorrión o puede ser un pingüino al mismo tiempo sin perder lo que es su esencia, que es ser un pájaro.

 1# Creamos la clase Pájaro
 2class Pajaro:
 3
 4  # Método volar
 5  def nadar(self):
 6    print("Puedo nadar.")
 7
 8  def volar(self):
 9    print("Puedo volar.")
10
11class Pinguino(Pajaro):
12  def nadar(self):
13    print("Puedo nadar.")
14
15  def volar(self):
16    print("No puedo volar.")
17
18def ver_volar(birds)
19  birds.volar()
20
21gorrion=Pajaro()
22pinguino=Pinguino()
23
24# Este imprimirá que pude volar
25ver_volar(gorrion)
26
27# Este imprimirá que no puede volar
28ver_volar(pinguino)