Clases y objetos

Esta es la definición más simple de una clase:

class Alumno:
    pass

Ciclo de vida de un objeto

class Alumno:
    # Metodo constructor
    def __init__(self):
        print(‘Objeto construido’)

        # Metodo destructor
        def __del__(self):
	    print(‘Objeto destruido’)

# Implementacion de la clase
alumno1 = Alumno()  # Objeto se construye
alumno1 = 10  # Objeto destruido

self

Para acceder elementos internos del objeto dentro de un método se utiliza el parámetro self, el cual es el primero que se coloca. Puede utilizarse cualquier palabra sin embargo por convención se recomienda la palabra self. Al implementarse la función este parámetro ya no es utilizado.

class Alumno:
    def matricular(self, codigo, seccion):
        self.codigo = codigo
	      self.seccion = seccion

    def saludar(self)
        print(‘Hola’)

Atributos

Existen dos tipos de atributos:

  1. Atributos de clase: Compartidos por todas las instancias de clase. Existe una copia de estas variables y los cambios producidos a estas variables son percibidas por todas las instancias.
  2. Atributos de instancia o de objeto: Cada objeto tiene una copia de la variable.
class Persona:
    poblacion = 0  # atributo de clase

    def __init__(self, nombre):
        self.nombre = nombre  # atributo de instancia
	      Persona.poblacion += 1

Métodos

Hay varios tipos de métodos:

class Persona:
    poblacion = 0

    def __init__(self, nombre):
        self.nombre = nombre
	      Persona.poblacion += 1
 
    @classmethod
    def conteo_personas(cls):  # cls representa la propia clase
        print(“Poblacion actual:”, cls.poblacion)

 
class Persona:
    poblacion = 0

    def __init__(self, nombre):
        self.nombre = nombre
        Persona.poblacion += 1
	
    @staticmethod
    def reiniciar_poblacion():
        Persona.poblacion = 0

Comparar objetos

Es importante establecer cuando dos objetos tienen el mismo valor y cuando son el mismo objeto (misma dirección de memoria).

str1 = “Hola”
str2 = “Hola”
str1 == str2  # True: tienen el mismo valor
str1 is str2  # True: son el mismo objeto

En el siguiente ejemplo se disponen dos objetos para comparar valores y direcciones de memoria.

class Auto:
    def __init__(self, marca, modelo, año):
        self.marca = marca
	self.modelo = modelo
	self.año = año

# Implementar de los objetos
corolla2016_1 = Auto(‘Toyota’, ‘Corolla’, 2016)
corolla2016_2 = Auto(‘Toyota’, ‘Corolla’, 2016)
print(corolla2016_1 == corolla2016_2)  # False

Se debe ajustar la equivalencia entre los objetos, en este caso se redefine el concepto de igualdad para establecer que dos objetos auto son iguales si tienen la misma marca, modelo y año respectivamente:

class Auto:
    def __init__(self, marca, modelo, año):
        self.marca = marca
	      self.modelo = modelo
	      self.año = año

    def __eq__(self, obj):
	      return self.marca == obj.marca and self.modelo == obj.modelo and self.año == obj.año

# Implementacion de los objetos
corolla2016_1 = Auto(‘Toyota’, ‘Corolla’, 2016)
corolla2016_2 = Auto(‘Toyota’, ‘Corolla’, 2016)

print(corolla2016_1 == corolla2016_2)  # True
print(corolla2016_1 is corolla2016_2)  # False

Copia de objetos

Del ejemplo anterior si realizamos las siguientes acciones:

auto1 = Auto(‘Honda’, ‘Civic’, 2015)
auto2 = auto1
auto1 == auto2	# True: son iguales
auto1 is auto2	# True: son el mismo objeto

auto2.año = 2010
print(auto1.año)	# Imprime 2010!

En este caso no se copiaron atributos, se copia la direccion de memoria lo que logra que ambos objetos sean el mismo. Por eso el operador is devuelve True.

Esto puede ser empleado si deseamos establecer estructuras de datos complejas como listas enlazadas, pero en el caso anterior parece ser poco útil.

Encapsulamiento

Por defecto todos los atributos son públicos en Python, cada mecanismo interno es revelado. Sin embargo, pueden ocultarse, por convención, algunos elementos.

Si utiliza un guión bajo (_) el atributo o el método usted le indica a otro programador que no debería accederse de forma externa ese elemento. Según la fuente este puede interpretarse como un elemento privado o protegido (aunque realmente no funciona como tal).

Utilizando doble guión bajo (__) se puede establecer que un atributo sea menos público. Ocurre una ofuscación donde para acceder de forma externa se necesita el siguiente patrón: objeto._NombreClase__AtributoOculto

class Contador:
    def __init__(self):
        self.__conteo = 0

    def contar(self):
        print (self.__conteo)

Propiedades

La mejor práctica si se desea el encapsulamiento es declarar el atributo como no público y establecer una función que permite leer (getter) y otra que permita escribir (setter). Python tambien tiene una opcion mas compacta en su uso: propiedades donde oculta ambas funcionalidad como si estuviera utilizando un atributo publico.

class Contador:
    def __init__(self):
        self.__conteo = 0
    @property
    def conteo(self):
        return self.__conteo

    @conteo.setter
    def conteo(self, valor):
        self.__conteo = valor

Herencia

La herencia es una característica propia del paradigma orientado a objetos. Ya que Python es un lenguaje basado en dicho paradigma y la mayoría de estructuras proceden de una clase.

Para determinar la herencia se encierra dentro de un paréntesis la clase de la cual se hereda la estructura.

class Derivada (Base):
    pass

Para acceder a los elementos de la clase padre, es necesario utilizar la función super() la cual se dirige a la clase padre. La función llamada no necesita utilizar la palabra self.

class Base:	
    def saludar(self):
        print('Metodo de la clase Base')


class Derivada(Base):
    def saludar(self):
        super().saludar()  # Utiliza la de clase base

Herencia Múltiple

En Python es posible que una clase tenga más de una clase base.

class A:
    def __init__(self):
        print('A')


class B:
    def __init__(self):
        print('B')


class C(B):
    def __init__(self):
        print('C')


class D(C, B, A):
    pass


# Implementacion del objeto
d0 = D()	

MRO (Method Resolution Order)

Para identificar en la herencia múltiple qué método de las clases que se heredan, es posible utilizar el MRO para indicar el orden de asignación.

Clase.mro()

Clase.__mro__

El orden también depende de cómo la clase base define el método a utilizar:

Es posible que, de acuerdo a la combinación de las clases bases, sea imposible obtener una resolución en el orden. En ese caso se genera una excepción. Sin embargo, cabe mencionar que las nuevas versiones de Python han robustecido el manejo de estos problemas. A continuación se muestra un ejemplo de fallo en la herencia:

class A:
    def __init__(self):
        print('A')


class B:
    def __init__(self):
        print('B')


class C(B):
    def __init__(self):
        print('C')


class D(A, B, C):
    pass

# TypeError: Cannot create a consistent method resolution order (MRO) for bases object, B, C

Clases Abstractas

A esto se lo llama una clase abstracta. Al contrario de las abstractas, las clases que sí son instanciables, se las denomina concretas.

Una clase abstracta define sólo la interfaz o firma de algunos de sus métodos. Podemos tener:

Manteniendo el concepto, una clase abstracta puede obligar a que sus subclases implementen cierto método, pero sin definir ningún comportamiento predeterminado. A estos métodos, naturalmente, se los denomina métodos abstractos.

Si una clase tiene al menos UN método abstracto, entonces debe ser abstracta, porque no tiene sentido que haya un objeto que sea instancia de ella.

Clases abstractas en Python

Python no soporta de forma nativa las clases abstractas. Sin embargo, es posible implementarlas debido al paquete abc (Abstract Base Classes), utilizando metaclases y decoradores.

Las metaclases son estructuras que instancian clases en lugar de objetos. Mientras que los decoradores agregan funcionalidad a las funciones mediante envoltorios (wrappers).

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        pass


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2


class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

# Intentar crear una instancia de la clase Shape directamente causará un error.
# shape = Shape()  # Esto generaría un TypeError

circle = Circle(5)
rectangle = Rectangle(4, 6)

print("Área del círculo:", circle.area())
print("Área del rectángulo:", rectangle.area())

Polimorfismo

Diferentes comportamientos ocurren dependiendo cual subclase es utilizada, sin tener que conocer cuál es la subclase.

Imagine que una clase denominada AudioFile carga un objeto y es capaz de utilizarlo mediante el método play(), el cual es responsable de descomprimir o extraer el archivo de audio y enrutarlo hacia la tarjeta de sonido y los parlantes. Por lo que utilizar un archivo de audio sería:

audio_file_obj.play()

El proceso de extracción es diferente para cada tipo de archivo. El .wav no es comprimir, pero sí lo son los .mp3, .ogg, .wma los cuales poseen diferentes tipos de compresión.

class AudioFile:
    def __init__(self, nombre_archivo):
        if not nombre_archivo.endswith(self.ext):
            raise Exception('Formato del archivo inválido')

        self.nombre_archivo = nombre_archivo


class MP3File(AudioFile):
    ext = "mp3"

    def play(self):
        print('.....Cargando {} como mp3'.format(self.nombre_archivo))


class WavFile(AudioFile):
    ext = "wav"

    def play(self):
        print('.....Cargando {} como wav'.format(self.nombre_archivo))


class OggFile(AudioFile):
    ext = "ogg"

    def play(self):        
        print('.....Cargando {} como ogg'.format(self.nombre_archivo))


# Función fuera de cualquier clase
def play(obj):
    obj.play()


ogg = OggFile('cancion.ogg')
mp3 = MP3File('cancion.mp3')
wav = WavFile('cancion.wav')

play(ogg)
play(mp3)
play(wav)

Duck typing

Además Python lleva al extremo el concepto de polimorfismo al aplicar duck typing. La idea detrás del Duck Typing es que «si se ve como un pato y suena como un pato, entonces debe ser un pato». En otras palabras, en lugar de depender del tipo declarado de un objeto, el Duck Typing se basa en si el objeto puede realizar las acciones necesarias de manera coherente.

Siguiendo el ejemplo anterior:

class NoAudio:
    def play(self):
        print('No audio')

no_audio = NoAudio()
play(no_audio)

Si se quiere impedir el duck typing se puede establecer alguna validación de los tipos a implementar:

def play(obj):
    if isinstance(obj, AudioFile):
        obj.play()
    else:
        raise Exception('Objeto indefinido')


no_audio = NoAudio()
play(no_audio)  # Exception: Objeto indefinido

Deja una respuesta

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