Python Para Principiantes (XI) - Programación orientada a objetos

Introducción a la programación orientada a objetos

Índice

Introducción

Este es otro paradigma distinto a los que ya conocemos que son: el funcional y el imperativo.

Un objeto se usa creando clases.

Las clases son los elementos fundamentales de la POO (Programación orientada a objetos).

Una clase describe al objeto.

Imaginemos que queremos describir a un perro.

Podemos pensar que tiene alguna características como, raza, color, peso y nombre, seguramente tendrá varias mas, pero de momento trabajemos con esto.

En Python las clases se declaran mediante el uso de la palabra reservada class

1
2
class Perro:
    pass

En el código anterior hemos declarado una clase Perro, pero de momento no hace nada.

En Python se acostumbra a que el nombre de la clase comience con una letra mayúscula.

Dijimos entonces que nuestro perro tiene ciertos atributos, raza, color peso y nombre, para introducirlos dentro de nuestra clase podemos hacer lo siguiente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Perro:   
    def __init__(self,raza,color,peso,nombre):
        self.raza = raza
        self.color = color
        self.peso = peso
        self.nombre = nombre
"""
En la siguiente línea instanciamos un objeto de la clase Perro
Esto se hace respetando esta forma: 
variable = Clase(argumento1, argumento2, ...)
Al crear el objeto automaticamente estamos invocando a __init__
En este caso a __init__ envíamos 4 argumentos
raza, color, peso y nombre
estos argumentos son los que estan definidos en el metodo __init__
dentro de la clase Perro
"""
miPerro = Perro("Cocker","Marrón",12,"Garabo") 
#Mostramos en pantalla el nombre de
print("El nombre de mi perro es \"",miPerro.nombre,"\"")
El nombre de mi perro es " Garabo "

Antes de continuar vamos a establecer ciertas reglas de nombres.

  • Si una clase contiene una función la llamamos método.
  • Si una clase contiene una variable la llamamos atributo.

Bien, tomando como referencia el código anterior lo primero que observamos es un método(una función) que se llama _init_ este método es conocido como **Constructor**

La declaración de un constructor NO ES obligatoria, es opcional. Sirve, particularmente, para inicializar atributos en el objeto.

1
    def __init__(self,raza,color,peso,nombre):

Este método se invoca al momento de crear (instanciar) un objeto.

En Python, todos los métodos de una clase deben tener self como primer parámetro.

En la línea

1
miPerro = Perro("Cocker","Marrón",12,"Garabo")

Lo que estamos haciendo es crear un objeto de la clase perro y le enviamos los argumentos necesarios que son los que fueron definidos en el método _init_, ellos son raza, color, peso y nombre.

Nótese que el primer argumento self no necesita ser explícitamente enviado.

Esos argumentos, se asignan dentro de los atributos de la clase como indica el siguiente fragmento de código:

1
2
3
4
        self.raza = raza
        self.color = color
        self.peso = peso
        self.nombre = nombre

Es decir que:

  • “Cocker” se guardó en self.raza
  • “Marrón” se guardó en self.color
  • 12 se guardó en self.peso
  • “Garabo” se guardó en self.nombre

Finalmente cuando hacemos:

1
print("El nombre de mi perro es",miPerro.nombre)

Estamos mostrando el valor asignado en el atributo self.nombre del objeto que creamos, en este ejemplo el atributo self.nombre del objeto en cuestión esta cargado con el valor “Garabo”.

Métodos

Las clases pueden tener métodos (funciones) acorde a las necesidades de las mismas.

Estos métodos son idénticos a los que venimos trabajando a lo largo del taller.

Por ejemplo la clase Perro puede tener un método que muestre el nombre del perro.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Perro:   
    def __init__(self,raza,color,peso,nombre):
        self.raza = raza
        self.color = color
        self.peso = peso
        self.nombre = nombre
        
    def nombre_del_perro(self):
        print("Me llamo", self.nombre) #Método que muestra el nombre del perro en pantalla
        
#Instanciamos un objeto de la clase Perro
miPerro = Perro("Cocker","Marrón",12,"Garabo")
miPerro.nombre_del_perro()
#Creamos otro objeto distinto de la misma clase
otroPerro = Perro("Gran Danes","Blanco y negro",48,"Bufa")
otroPerro.nombre_del_perro()
Me llamo Garabo
Me llamo Bufa

Nótese como podemos crear diversos objetos del tipo Perro con solo hacer:

1
objeto = Clase(argumentos)

Recuerden que para acceder a un método hacemos

1
2
3
objeto.metodo() 
# o bien
objeto.metodo(arg1, ...) #Si le pasamos argumentos

Atributos

Las clases, como vimos pueden tener atributos, que se inicializan al momento de instanciar el objeto

1
2
3
def __init__(self, atributo1, atributo2):
        self.atributo1 = atributo1
        self.atributo2 = atributo2

Además, puede tener atributos definidos por defecto.

En el caso de los perros tienen, como atributo definido por defecto, cero manchas.

Entonces podemos hacer lo siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Perro:   
    #Atributo previamente definido
    manchas = 0
    def __init__(self,raza,color,peso,nombre):
        self.raza = raza
        self.color = color
        self.peso = peso
        self.nombre = nombre
        
    def nombre_del_perro(self):
        print("Me llamo", self.nombre) #Método que muestra el nombre del perro en pantalla       

        
#Instanciamos un objeto de la clase Perro
miPerro = Perro("Cocker","Marrón",12,"Garabo")
#Mostramos las manchas
print(miPerro.manchas)
#Si queremos modificar las manchas que tiene el animal
miPerro.manchas = 12
print(miPerro.manchas)
0
12

Funciones para poner y devolver datos.

Suele ser una buena idea pensar en los métodos de dos maneras posibles. Algunos métodos devuelven resultados o datos y otros asignan.

Veamos un ejemplo en nuestra clase perro. Supongamos que no queremos, al momento de instanciar el objeto, enviarle argumento alguno, pero si queremos tener la posibilidad de ir poniendo valor a “mano”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Perro:
    nombre = ""
    raza = ""
    color = ""
    peso = ""
    def __init__(self): #El constructor está vacío
        pass
    
    def poner_nombre(self,nombre): #Este metodo asigna
        self.nombre = nombre
        
    def dame_nombre(self): #Este metodo retorna
        return self.nombre

miPerro = Perro()
print(miPerro.dame_nombre()) #No imprime nada porque de momento nombre está vacío
miPerro.poner_nombre("Garabo") #Asigna el argumento enviado en la variable nombre del objeto
print(miPerro.dame_nombre()) #Debe imprimir el nombre asignado
Garabo

¿Qué es self ?

Si han estado observado detalladamente notarán el uso de la palabra reservada self en varias ocasiones.

Pensemos en lo siguiente.

1
2
3
4
5
6
7
class Perro:
    nombre = "" #aquí nombre es un atributo de la clase Perro
    def __init__(self, nombre): #aquí nombre es un parametro de la función
        self.nombre = nombre #Se asigna el valor del parametro "nombre" al atributo "nombre" de la clase
            
miPerro = Perro("Pirulo")
print(miPerro.nombre)
Pirulo

La pregunta es:

¿Como sabe el programa distinguir el parámetro nombre de la función init, del atributo nombre definido en la clase Perro?

Muy simple, mediante el uso de self.

Cuando vemos self.atributo nos referimos al atributo de clase y NO al argumento del método.

En esta línea

1
self.nombre = nombre 

self.nombre es el atributo definido adentro de la clase y nombre es simplemente el argumento que enviamos, en este caso “Pirulo”.

Es decir que el valor “Pirulo” se guardará en el atributo “nombre” (definido en la segunda línea del código) del objeto que hemos creado.

Herencia

La herencia es un mecanismo que nos facilita compartir información entre distintas clases.

Por ejemplo, los animales comparten ciertos atributos, color, peso, nombre. Pero también tienen diferencias, el gato maúlla y el perro ladra por ejemplo.

Esto se puede expresar creando una clase madre donde se definen “las cosas que comparten” y clases hijas en donde se definen las diferencias. Todas las clases hijas tomarán (heredan) de la madre todos lo que se encuentre definido en ella.

Veamos un ejemplo para que quede mas claro:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Clase animales
class Animales:
    #elementos en común
    color = ""
    peso = ""
    nombre = ""
    def __init__(self,color,peso,nombre):
        self.color = color
        self.peso = peso
        self.nombre = nombre

class Perro (Animales): #Entre parentesis le estoy indicando cual es la clase madre
    def ladrar(self):
        print("Guau!")
        
class Gato (Animales):
    def maullar(self):
        print("Miau")
        
miPerro = Perro("Marron",12,"Garabo") #Objeto de la clase perro
miGata = Gato("Gris",6,"Luna") #Objeto de la clase gato
miPerro.ladrar()
miGata.maullar()
print(miGata.nombre)
print(miPerro.nombre)
Guau!
Miau
Luna
Garabo

Podemos pensarlo “como si” el contenido de la clase Animales se hubiera “copiado” dentro de la clase Perro y dentro de la clase Gato

Herencia indirecta

La herencia también puede ser indirecta como se ilustra en el siguiente ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Base:
    def metodoA(self):
        print("Soy la madre")
    
class Primera(Base):
    def metodoB(self):
        print ("Soy la Primera")
        
class Segunda(Primera):
    def metodoC(self):
        print("Soy la Segunda")
        
segunda = Segunda()
segunda.metodoC()
segunda.metodoB()
segunda.metodoA()
Soy la Segunda
Soy la Primera
Soy la madre

Palabra reservada super

La palabra reservada super() sirve para referirnos al método definido en la clase madre. Por ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Base:
    def saludar(self):
        print("Soy la clase base")
        
class Primera(Base): 
    def saludar(self):
        print("Soy la clase Primera")   
        super().saludar() #Invoca a saludar() de la clase Base

objeto = Primera()
objeto.saludar()

Soy la clase Primera
Soy la clase base

Explicación del ciclo de vida de un objeto.

A grandes rasgos podemos decir que el ciclo de vida de un objeto consta de tres etapas:

La creación, la manipulación y la destrucción del objeto.

Habitualmente la primer actividad que hacer hacemos al escribir código en base a este paradigma (POO) es definir la clase a la cual el objeto pertenece.

Luego instanciamos el objeto.

1
objeto = Clase()

En este momento se invoca a la función _init_ y se aloca (se reserva) memoria para esta instancia. Después de esto el objeto esta listo para ser usado.

Cuando destruimos un objeto, la memoria alocada se libera.

Ahora bien, un objeto se destruye completamente, cuando el “conteo de referencia” llega a cero, pero ¿Que es el conteo de referencia?

Es el numero de variables y objetos a los que ese elemento se refiere.

A modo de ejemplo

1
2
3
4
num = 10 #Creamos un objeto tipo int <10>
num2 = num1 #Aumenta una referencia a int <10>
diez = num2 #Aumenta una referencia a int <10>
del num2 #Decrece la referencia a int <10>

Es decir, mientras más objetos o variables asociados a num tengamos a lo largo del programa, más referencias tenemos, el operador “de"l borra la referencia, cuando ese conteo de referencias este en 0, se considera (automáticamente) que esa memoria no se necesita mas y por lo tanto, el “Recolector de basura” de Python liberará completamente la memoria.

Este tema tiene mayor profundidad, pero la idea del taller es darle una mínima introducción a este tema que se relaciona con el manejo de la memoria en Python.

Encapsulamiento

El encapsulamiento es una característica que suelen tener los lenguajes orientados a objetos. Se basa en un concepto conocido como ocultamiento de datos el cual establece que los detalles de la implementación de una clase deben estar ocultos para aquellos que hagan uso de la clase en cuestión. En lenguajes de programación, como C++, hacemos uso de ciertas palabras reservadas como public o private para determinar que elementos están ocultos y cuales no.

En Python este acercamiento se hace desde otro punto de vista, la filosofía del lenguaje establece que “somos todos adultos” por lo tanto no tiene sentido poner restricciones de acceso a las clases, en consecuencia, no hay maneras de forzar que un método o atributo sea estrictamente privado.

Sin embargo, hay una pequeña manera de denegar el acceso a métodos y atributos y consiste en escribir __ (dos guiones bajos) delante del nombre del atributo.

1
2
3
4
5
class Unaclase:
    __atributo_privado = "Solo puede ser accedido desde la clase misma"

miObjeto = Unaclase()
print(miObjeto.__atributo_privado)
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-2-c664b4ef8e15> in <module>
      3 
      4 miObjeto = Unaclase()
----> 5 print(miObjeto.__atributo_privado)


AttributeError: 'Unaclase' object has no attribute '__atributo_privado'

De todas formas, este tipo de implementación en Python no es recomendable.

Hasta aquí llegará nuestro taller. Si bien han quedado algunos temas afuera entiendo que se han brindado las herramientas fundamentales para comenzar a construir programas Python. A partir de esto punto pueden seguir investigando y aprendiendo sobre el lenguaje, hay diversidad de temas para conocer:

  • Frameworks que permiten hacer web dinámicas Flask y Django o estáticas Pelican/Lekor/Nikola

  • Herramientas para crear interfaces gráficas Qt, Pysdl, tkinter

  • Módulos interesantes para trabajar la web beatifull soup o para análisis de datos/computación científica pandas/numpy

  • Para hacer juegos pygame o desarrollo para Android kivy.

En fin, tienen infinidad de opciones para seguir investigando y desarrollándose. Espero que el taller les haya sido de utilidad y si me ven en algún espacio de los que suelo trabajar (Universidades o Empresas) no duden en saludarme. Les dejo mi correo electrónico para cualquier consulta que tengan.

victorsaturno@disroot.org

Sin más me despido de ustedes y que tengan éxitos en los caminos que emprendan.

updatedupdated2021-02-032021-02-03