Translate

lunes, 3 de marzo de 2025

Programación Orientada a Objetos en Python parte 2


Ya vimos lo complejo que es crear software y como la programación orientada a objetos nos ayuda.

Ahora veamos como se aplican en Python, empecemos con las reglas de Nomenclatura.

En Python existen convenciones de nomenclatura recomendadas por la PEP 8, que es la guía de estilo oficial del lenguaje. Las reglas principales para definir clases, métodos y atributos:

Nombres de Clases  

- Se usa PascalCase (también conocido como **CamelCase** con la primera letra en mayúscula).  

- Cada palabra comienza con mayúscula, sin guiones bajos.  

Por ejemplo: 

  class MiClase:

      pass


Nombres de Métodos y Atributos Publicos  

- Se usa snake_case (minúsculas separadas por guiones bajos).  

Por ejemplo:

  class Persona:

      def obtener_nombre(self):

          return "Juan"


Nombres de Métodos y Atributos Privados

- Se usa un guion bajo inicial (_) para indicar que es un atributo/método de uso interno (convención, no es privado en sentido estricto).  

Por ejemplo:

  class Persona:

      def _calcular_edad(self):

          return 30


Atributos y Métodos Realmente Privados  

- Se usa doble guion bajo (`__`) para evitar colisiones de nombres en clases hijas (_name mangling_).  

Por ejemplo:

  class Persona:

      def __metodo_secreto(self):

          print("Este método es realmente privado")


Atributos y Métodos Estáticos o de Clase  

- Métodos de clase (@classmethod) y atributos de clase siguen la convención snake_case.  

- Métodos estáticos (@staticmethod) también siguen snake_case.

Por ejemplo:

  class Utilidades:

      @classmethod

      def metodo_de_clase(cls):

          pass


      @staticmethod

      def metodo_estatico():

          pass


Constantes

- Se usan mayúsculas con guiones bajos.  

Por ejemplo:

  class Config:

      MAX_INTENTOS = 5


Definiendo clases

Vista ya la parte teórica, vamos a ver como podemos hacer uso de la programación orientada a objetos en Python. Lo primero es crear una clase, para ello usaremos el ejemplo de perro.


# Creando una clase vacía

class Perro:

    pass


Se trata de una clase vacía y sin mucha utilidad práctica, pero es la mínima clase que podemos crear. Nótese el uso del pass que no hace realmente nada, pero daría un error si después de los : no tenemos contenido.

Ahora que tenemos la clase, podemos crear un objeto de la misma. Podemos hacerlo como si de una variable normal se tratase. Nombre de la variable igual a la clase con (). Dentro de los paréntesis irían los parámetros de entrada si los hubiera.


# Creamos un objeto de la clase perro

mi_perro = Perro()


Definiendo atributos

A continuación vamos a añadir algunos atributos a nuestra clase. Antes de nada es importante distinguir que existen dos tipos de atributos:

Atributos de instancia: Pertenecen a la instancia de la clase o al objeto. Son atributos particulares de cada instancia, en nuestro caso de cada perro.

Atributos de clase: Se trata de atributos que pertenecen a la clase, por lo tanto serán comunes para todos los objetos.

Empecemos creando un par de atributos de instancia para nuestro perro, el nombre y la raza. Para ello creamos un método __init__ que será llamado automáticamente cuando creemos un objeto. Se trata del constructor.


class Perro:

    

    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


Ahora que hemos definido el método init con dos parámetros de entrada, podemos crear el objeto pasando el valor de los atributos. Usando type() podemos ver como efectivamente el objeto es de la clase Perro.


mi_perro = Perro("Toby", "Bulldog")

print(type(mi_perro))

# Creando perro Toby, Bulldog

# <class '__main__.Perro'>


Seguramente te hayas fijado en el self que se pasa como parámetro de entrada del método. Es una variable que representa la instancia de la clase, y deberá estar siempre ahí.

El uso de __init__ y el doble __ no es una coincidencia. Cuando veas un método con esa forma, significa que está reservado para un uso especial del lenguaje. En este caso sería lo que se conoce como constructor. 

Por último, podemos acceder a los atributos usando el objeto y . (el punto)


print(mi_perro.nombre) # Toby

print(mi_perro.raza)   # Bulldog


Hasta ahora hemos definido atributos de instancia, ya que son atributos que pertenecen a cada perro concreto. Ahora vamos a definir un atributo de clase, que será común para todos los perros. Por ejemplo, la especie de los perros es algo común para todos los objetos Perro.


class Perro:

    # Atributo de clase

    especie = 'mamífero'


    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


Dado que es un atributo de clase, no es necesario crear un objeto para acceder al atributos. Podemos hacer lo siguiente.


print(Perro.especie)

# mamífero


Se puede acceder también al atributo de clase desde el objeto.


mi_perro = Perro("Toby", "Bulldog")

mi_perro.especie

# 'mamífero'


De esta manera, todos los objetos que se creen de la clase perro compartirán ese atributo de clase, ya que pertenecen a la misma.


Definiendo métodos

En realidad cuando usamos __init__ anteriormente ya estábamos definiendo un método, solo que uno especial. A continuación vamos a ver como definir métodos que le den alguna funcionalidad interesante a nuestra clase, siguiendo con el ejemplo de perro.

Vamos a codificar dos métodos, ladrar y caminar. El primero no recibirá ningún parámetro y el segundo recibirá el número de pasos que queremos andar. Como hemos indicado anteriormente self hace referencia a la instancia de la clase. Se puede definir un método con def y el nombre, y entre () los parámetros de entrada que recibe, donde siempre tendrá que estar self el primero.


class Perro:

    # Atributo de clase

    especie = 'mamífero'


    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


    def ladra(self):

        print("Guau")


    def camina(self, pasos):

        print(f"Caminando {pasos} pasos")


Por lo tanto si creamos un objeto mi_perro, podremos hacer uso de sus métodos llamándolos con . y el nombre del método. Como si de una función se tratase, pueden recibir y devolver argumentos.


mi_perro = Perro("Toby", "Bulldog")

mi_perro.ladra()

mi_perro.camina(10)


# Creando perro Toby, Bulldog

# Guau

# Caminando 10 pasos


Métodos en Python: instancia, clase y estáticos

Es posible crear diferentes tipos de métodos:

  • Lo métodos de instancia “normales” que ya hemos visto como metodo()
  • Métodos de clase usando el decorador @classmethod
  • Y métodos estáticos usando el decorador @staticmethod

En la siguiente clase tenemos un ejemplo donde definimos los tres tipos de métodos.


class Clase:

    def metodo(self):

        return 'Método normal', self


    @classmethod

    def metododeclase(cls):

        return 'Método de clase', cls


    @staticmethod

    def metodoestatico():

        return "Método estático"


Veamos su comportamiento en detalle uno por uno.


Métodos de instancia

Los métodos de instancia son los métodos normales, de toda la vida, que hemos visto anteriormente. Reciben como parámetro de entrada self que hace referencia a la instancia que llama al método. También pueden recibir otros argumentos como entrada.

Para saber más: El uso de "self" es totalmente arbitrario. Se trata de una convención acordada por los usuarios de Python, usada para referirse a la instancia que llama al método, pero podría ser cualquier otro nombre. Lo mismo ocurre con "cls", que veremos a continuación.


class Clase:

    def metodo(self, arg1, arg2):

        return 'Método normal', self


Y como ya sabemos, una vez creado un objeto pueden ser llamados.


mi_clase = Clase()

mi_clase.metodo("a", "b")

# ('Método normal', <__main__.Clase at 0x10b9daa90>)


En vista a esto, los métodos de instancia:

  •  Pueden acceder y modificar los atributos del objeto.
  •  Pueden acceder a otros métodos.
  •   Dado que desde el objeto self se puede acceder a la clase con ` self.class`, también pueden modificar el estado de la clase


Métodos de clase (classmethod)

A diferencia de los métodos de instancia, los métodos de clase reciben como argumento cls, que hace referencia a la clase. Por lo tanto, pueden acceder a la clase pero no a la instancia.


class Clase:

    @classmethod

    def metododeclase(cls):

        return 'Método de clase', cls


Se pueden llamar sobre la clase.


Clase.metododeclase()

# ('Método de clase', __main__.Clase)


Pero también se pueden llamar sobre el objeto.


mi_clase.metododeclase()

# ('Método de clase', __main__.Clase)


Por lo tanto, los métodos de clase:

  • No pueden acceder a los atributos de la instancia.
  • Pero si pueden modificar los atributos de la clase.
  • Métodos estáticos (staticmethod)

Por último, los métodos estáticos se pueden definir con el decorador @staticmethod y no aceptan como parámetro ni la instancia ni la clase. Es por ello por lo que no pueden modificar el estado ni de la clase ni de la instancia. Pero por supuesto pueden aceptar parámetros de entrada.


class Clase:

    @staticmethod

    def metodoestatico():

        return "Método estático"


mi_clase = Clase()

Clase.metodoestatico()

mi_clase.metodoestatico()


# 'Método estático'

# 'Método estático'


Por lo tanto el uso de los métodos estáticos pueden resultar útil para indicar que un método no modificará el estado de la instancia ni de la clase. Es cierto que se podría hacer lo mismo con un método de instancia por ejemplo, pero a veces resulta importante indicar de alguna manera estas peculiaridades, evitando así futuros problemas y malentendidos.

En otras palabras, los métodos estáticos se podrían ver como funciones normales, con la salvedad de que van ligadas a una clase concreta.