Programación orientada a objetos con Python

Python es un lenguaje multiparadigma por lo que también se desarrolló para la programación orientada a objetos (POO). Un objeto es una instancia creada en base a una clase a partir de un concepto, puede ser un coche, una persona, un televisor, etc.

Podemos resumirlo como que una clase es una plantilla o un modelo, y un objeto, es una copia de la misma pero con unos atributos definidos desde la propia clase.

Clases y objetos control de flujo

En otros artículos hemos hablado sobre las diferentes estructuras de datos que existen en Python, como son las listas, diccionarios, tuplas o conjuntos. Pues bien, ahora aprenderás a crear tus propias estructuras gracias a las clases, objetos y métodos de la clase.

Definiendo una clase

Una clase es una especie de plantilla genérica de la cual iremos creando copias (instancias) con unos atributos específicos y que definiremos desde la propia clase. Además, las instancias de la clase pueden contener métodos para especificar las posibles modificaciones de dichos objetos.

Nota

Cada vez que se construye un objeto de una clase se crea una instancia de la misma.

Para crear una clase necesitamos hacer uso de la palabra reservada class seguida del nombre de la clase y dos puntos, según la siguiente sintaxis:

class User:
    <declaración - 1>
    <declaración - N>

Recomendación

Es recomendable y es una buena práctica que el primer carácter del nombre de la clase comience por mayúscula, es decir, en Pascal Case.

Atributos de una clase

Los atributos o propiedades son las características que puede tener dicho objeto, es decir, son las variables que definen o describen al objeto de la clase.

class User:
    """ Clase que representa un usuario """
    name = "John Doe"
    mail = "[email protected]"
    age = 35
    active = True

Como puede verse en el ejemplo, los atributos para esta clase es la información personal de un usuario. Si queremos obtener el atributo name de la clase User, solo tenemos que llamar al objeto de la clase seguido de un punto y el atributo.

new_user = User
print(new_user.name) 
# Salida
John Doe

Si vienes de otros lenguajes de programación que también están orientados a objetos como lo es C++ o Java, el método constructor te será muy familiar, ya que cuando lo llamamos inicializa los atributos del objeto y cuyo primer parámetro siempre es self. En Python se consigue con el método especial __init__ del que hablaré más adelante.

class User:
    def __init__(self, name, email, age, status):
        self.name = name                       
        self.email = email
        self.age = age
        self.active = status

Ahora que ya tenemos el método constructor definido podemos llamar al objeto de la instancia self, con los atributos y la información que albergan estas variables.

name = "John Doe"                              
email = "[email protected]"
age = 35                        
status = True

new_user = User(name, email, age, status)
print(new_user.name)
print(new_user.email)
print(new_user.age)
print(new_user.active)
# Salida
John Doe
[email protected]
35
True

En este ejemplo definimos las variables con los valores de los atributos de la clase, es decir, los parámetros del método constructor. Después, llamamos al objeto new_user con estos parámetros y finalmente, lo inicializamos junto al atributo seguido de un punto.

Podríamos haber introducido los valores directamente en el objeto pero cuando estás trabajando en un proyecto real, lo más seguro es que recojas los datos de diferentes fuentes y sea una variable la que esté como parámetro y no el valor.

Recomendación

Podemos acceder a los atributos y valores del objeto con el método .__dict__, este devuelve un diccionario clave:valor.

new_user.__dict__
# Salida
{'name': 'John Doe', 
'email': '[email protected]',
'age': 35, 
'active': True}

El decorador @property

Con el decorador @property podemos modificar un método para que se convierta en un atributo y así, podremos acceder a él como si de un atributo se tratase, omitiendo el uso de los paréntesis.

¿Para qué? Esto nos permite tener un mayor control ocultando el estado de los objetos al exterior y así evitar modificaciones. Es decir, puede darse la ocasión en que queremos que nuestras variables estén ocultas y así, evitar que sean accesibles con el exterior.

class User:
    def __init__(self, number):
        self.__number = number
    
    @property
    def show_id(self):
        return self.__number  #.__ le dice a Python
                              # que el atr esté oculto
new_user = User(23334323)
print("Su Id es:", new_user.show_id)
# Salida
Id de usuario: 23334323

Nota

Aunque el atributo parezca un método no lo es, asi que no se le pueden aplicar parametros.


Métodos de una clase

En Python existen 3 tipos de métodos cuyas características son diferentes y se utilizan dependiendo el escenario en el que nos encontremos.

Estos son:

  1. Métodos de instancia
  2. Métodos de clase
  3. Métodos estáticos

Métodos de instancia

El método de instancia recibe como primer parámetro self, este hace referencia a la instancia del objeto, capaz de crear, modificar u obtener los atributos de la instancia de dicha clase.

Nota

self es un parámetro que puede ser llamado de cualquier otra forma, su uso es una convención por la comunidad de Python y está reconocido como una buena práctica.

class User:
    def __init__(self, name, email):
        self.name = name                   
        self.email = email

Ahora solo nos queda llamar al objeto:

name = "John Doe"                        
email = "[email protected]" 

new_user = User(name, email)
print(new_user) 
# Salida
John Doe
[email protected]

Métodos estáticos

En los métodos estáticos no se requiere pasar como parámetro el argumento self, sino que hay que utilizar el decorador @staticmethod para indicarle a Python de que se trata de un método estático. A diferencia de los métodos de instancia estos no tienen contacto con el exterior, así que para acceder a los atributos de la clase se debe de utilizar la clase como prefijo.

class User: 
    @staticmethod
    def user_description():
        return "Este es un método estático"

# Clase.métodoestático
description = User.user_description()
print(description)
# Salida
Este es un método estático

Métodos de clase

Este tipo de método recibe como parámetro cls, es decir la clase en sí, que como self puede llamarse de cualquier otra forma. Al igual que los métodos estáticos hay que utilizar un decorador, en este caso @classmethod. Una característica de este método es que no se puede acceder los atributos de la instancia pero si modificar los de la clase.

class User:
    @classmethod
    def welcome(cls, name):
        print("¡Hola {}!".format(name))
        return 

User.welcome("Diego")
# Salida
¡Hola Diego!

Métodos especiales

Los métodos especiales en Python son aquellos que están definidos en la clase, vulgarmente llamados «métodos mágicos» ya que estos, permiten que las clases actúen como iteradores, diccionarios o como funciones mismamente, entre otras.

En este artículo hablaré de tres métodos bastante utilizados y si quieres indagar más sobre los muchos otros métodos, puedes echar un vistazo a la documentación del modelo de datos de Python para ver una amplia lista.

El método __init__

Es el método constructor y se encuentra dentro de los métodos especiales, cuando se es llamado inicializa los atributos del objeto de la clase, cuyo primer parámetro siempre es self, ya explicado en los métodos de instancia.

class User:
    def __init__(self, name, email, active):    
        self.name = name                       
        self.email = email

Nota

Al llamar al método deben de coincidir el número de parámetros del mismo, sino __init__ requerirá el argumento de la posición.

El método __del__

Se trata del destructor del objeto y a diferencia de __init__, es llamada cuando la instancia va a ser eliminada. Esto quiere decir que no podríamos acceder al objeto de dicha instancia ya que obtendremos un error.

En el siguiente ejemplo utilizaremos el código que hemos estado usando en ejemplos anteriores y le añadimos el método. Por último, eliminamos el objeto new_user.

class User:
    def __init__(self, name, email, status):     
        self.name = name                        
        self.email = email

    def __del__(self):
        return print("Instancia borrada")

name = "John Doe" 
email = "[email protected]"                        

new_user = User(name, email)
del new_user        
# Salida
Instancia borrada

El método __str__

El método __str__ es un método que cuando se llama devuelve un string del objeto, por ejemplo, cuando hacemos un print en un formato tradicional: str(x)

def __str__(self):
    return ("Nombre: {}\nEmail: {}\nActivo: {}".format(self.name, self.email))

name = "John Doe"                               
email = "[email protected]"                          

new_user = User(name, email)         
print(new_user) 
# Salida 
Nombre: John Doe
Email: [email protected]

La herencia simple

La herencia es una técnica de la programación orientada a objetos que permite crear clases nuevas a partir de ya existentes. La clase principal se llama base y todas las que la heredan son clases derivadas. Para que te hagas una idea y no sea todo tan abstracto, el siguiente diagrama te facilita un poco la comprensión sobre cómo funcionan las herencias.

Diagrama herencia

Como ves su función principal es no repetir el mismo código de forma innecesaria pero además, tiene unas cuantas ventajas.

Ventajas sobre su uso

  • Permite reutilizar código (Don’t Repeat Yourself)
  • Comparte métodos y atributos de la base
  • Sobrescribe y crea nuevos métodos o atributos
  • Organización, código más limpio y legible

Su estructura es muy sencilla ya que se trata de una clase normal y otra que como parámetro tiene a la clase principal, veamos su sintaxis:

# Clase base
class Entertainment:
    pass

# Clase derivada
class Film(Entertainment):
    pass

Entendida esta parte vamos a ponerlo en práctica creando la clase Entertainment y luego las derivadas de esta con sus atributos y métodos.

class Entertainment:
    ''' Clase para Entertainment '''
    def __init__(self, title, genre, description):
        self.title = title
        self.genre = genre
        self.description = description

class Film(Entertainment):
    ''' Clase para Film '''
    def __init__(self, title, director, casting, genre, description):
        super().__init__(title, genre, description)
        self.director = director
        self.casting = casting

Como ves, la clase Entertainment no tiene ningún misterio según lo que hemos ido aprendido en puntos anteriores. Sin embargo la clase Film que como parámetro contiene a la clase base, necesita aplicar la función super() seguido del constructor para acceder a los métodos y atributos de esta.

A continuación hemos creado los nuevos atributos para la clase Film y así le mostramos al usuario la información de las películas de nuestra base de datos.

Por último llamamos al objeto y como parámetro el índice del elemento de la lista y así, obtenemos los datos en un nuevo diccionario gracias al método especial __dict__.

# Datos de la película
film_data = ["Gladiator", 
             "Ridley Scott", 
             ["Russell Crowe", "Joaquin Phoenix", "Connie Nielsen"], 
             "Acción", 
             "Máximo, general romano, desea volver a casa, pero el emperador Marco Aurelio quiere que herede el imperio..."]

# film_data[índice del elemento]
film = Film(film_data[0], film_data[1], film_data[2], film_data[3], film_data[4])
print(film.__dict__)
# Salida

{'title': 'Gladiator', 
 'genre': 'Acción', 
 'description': 'Máximo, general romano, desea volver a casa, pero el emperador Marco Aurelio quiere que herede el imperio...', 
 'director': 'Ridley Scott', 
 'casting': ['Russell Crowe', 'Joaquin Phoenix', 'Connie Nielsen']}

La herencia múltiple

La herencia múltiple tiene una característica particular que es que puede heredar de varias clases principales.

Nota

La clase derivada solo puede heredar los atributos y métodos de una de las clases principales y por consiguiente, utilizará el primer método o atributo heredado.

# Clase base 1
class FreeMusic:
    pass

# Clase base 2
class PremiumMusic:
    pass

# Clase base 3
class Plan(FreeMusic, PremiumMusic):
    pass

El polimorfismo

Se trata una técnica o método estrictamente ligado a la herencia que en muchos casos es aplicada a lenguajes de programación, cuyo paradigma está orientado a objetos. El polimorfismo permite que un método con un determinado nombre pueda ser utilizado en diferentes objetos.

Un buen ejemplo para entender el polimorfismo es la función len() que funciona tanto para cadenas de texto como para listas. Veamos un ejemplo para ponernos en situación y seguro que lo entenderás mejor.

# La función len() en un srtring
print(len("Polimorfismo"))

# La función len() en una lista
print(len(["P", "o", "l", "i", "m", "o", "r", "f", "i", "s", "m", "o"]))
# Salida 1
12
# Salida 2
12

Entendida esta parte, echemos un vistazo a un ejemplo práctico del cuál podrás poner en práctica tus habilidades, además, te recomiendo prestar especial atención al método plan() ya que según el plan que haya activado el usuario tendrá acceso a música gratis o al espacio premium.

Veamos en el siguiente código un ejemplo de herencia y polimorfismo.

class Radio:
    def __init__(self, status):
        self.status = status
    
    def plan(self):
        if self.status == False:
            print("--- Música gratis ---")
        elif self.status ==  True:
            print("--- Música Premium ---")
             
class FreeMusic(Radio):
    def __init__(self, status):
        super().__init__(status)

    def plan(self):
        count = 0
        upgrade = ['Música sin publicidad', 'Miles de canciones', 'Descarga tu lista favorita']
        if self.status == False:
            print('Te recomendamos suscribirte al plan Premium:')
            for item in upgrade:
                count += 1
                print(count, item)

class PremiumMusic(Radio):
    def __init__(self, status):
        super().__init__(status)

    def plan(self):
        if self.status == True:
            print('¡Disfruta del plan Premium!')

# Llamada al objeto
active = Radio(True)

# Activa el plan según el satus 
if active.status != True:
    active.plan()
    free_function = FreeMusic(False)
    free_function.plan()
elif active.status == True:
    active.plan()
    premium_function = PremiumMusic(True)
    premium_function.plan()

# Salida False
--- Música gratis ---
Te recomendamos suscribirte al plan Premium:
1 Música sin publicidad
2 Miles de canciones
3 Descarga tu lista favorita

# Salida True
--- Música Premium ---
¡Disfruta del plan Premium!

Como has podido ver Python es un lenguaje que para él, el tipo de objeto no es tan relevante como lo que puedan hacer sus métodos. Python soporta el duck typing o tipado de pato del que hablaremos en otro artículo y que no necesitas entender para el propósito de este tutorial.

Curiosidad

Según el razonamiento inductivo atribuido a James Withcomb Ryles, el nombre del concepto Duck Typing se refiere a un pato. Referenciamos estas palabras obtenidas de Wikipedia:

«Cuando veo un ave que camina como un pato, nada como un pato y suena como un pato, a esa ave yo la llamo un pato.»


Conclusión

En resumen, el paradigma POO es bastante amplio y fue popularizado a principios de los años 90 ya que lenguajes como Java, C++, Javascript, C#, PHP, Ruby, Scala o Python, fueron diseñados para ello.

Si buscas una infraestructura sólida, legible y donde el código no se convierta en un problema en la identificación y resolución de fallos, la programación estructurada y orientada a objetos podría ser una buena base para el desarrollo de tus aplicacones.

¿Te ha parecido útil?