miércoles, 3 de junio de 2015

Programación Orientada a Objetos (y III)



Polimorfismo: Sobrecarga de métodos (Overriding Methods)


La sobrecarga de métodos se refiere a la posibilidad de que una subclase cuente con métodos con el mismo nombre que los de una clase superior pero que definan comportamientos diferentes.

En el siguiente ejemplo se redefinen dos métodos que son heredados de clases superiores: __init__ y reproducirmp3()

class Movil(Telefono, Camara, Reproductor):
    def __init__(self):
        print('Móvil encendido')
    def reproducirmp3(self):
        print('Reproduciendo lista mp3')
    def __del__(self):
        print('Móvil apagado')

movil3 = Movil()  # Móvil encendido
movil3.reproducirmp3()  # Reproduciendo lista mp3
del movil3  # Móvil apagado

La siguiente lista enumera algunos métodos especiales que se pueden sobreescribir para que tengan comportamientos diferentes:

__init__ (self [,args...]): Método constructor que se ejecuta al crear un objeto. Sus argumentos son opcionales: objeto = NombreClase(argumentos)

__del__(self): Método destructor que se ejecuta al suprimir un objeto: del objeto

__repr__(self): Método que se ejecuta cuando se utiliza la función repr(objeto) para convertir datos del objeto a cadenas expresadas como representaciones legibles por el intérprete Python: repr(objeto)

__str__(self): Método que se ejecuta cuando imprimimos una instancia del objeto con print(objeto) o llamamos a la función str(objeto) para convertir datos a cadenas imprimibles. A diferencia de __repr__ la cadena resultante no necesita ser una expresión Python válida: str(objeto)

Polimorfismo: Sobrecarga de Operadores (Overloading Operators)


La sobrecarga de operadores trata básicamente de lo mismo que la sobrecarga de métodos pero pertenece en esencia al ámbito de los operadores aritméticos, binarios, de comparación y lógicos.

class Punto:
    def __init__(self,x = 0,y = 0):
        self.x = x
        self.y = y

    def __add__(self,other):
        x = self.x + other.x
        y = self.y + other.y
        return x, y

punto1 = Punto(4,6)
punto2 = Punto(1,-2)
print(punto1 + punto2)  # (5, 4)

Ocultación de datos (Encapsulación)


Los atributos de un objeto pueden ocultarse (superficialmente) para que no sean accedidos desde fuera de la definición de una clase. Para ello, es necesario nombrar los atributos con un prefijo de doble subrayado: __atributo

class Factura:
    __tasa = 19
 
    def __init__(self, unidad, precio):
        self.unidad = unidad
        self.precio = precio

    def a_pagar(self):
        total = self.unidad * self.precio
        impuesto = total * Factura.__tasa / 100
        return(total + impuesto)
 
compra1 = Factura(12, 110)
print(compra1.unidad)
print(compra1.precio)
print(compra1.a_pagar(), "euros") 
print(Factura.__tasa)  # Error:

# AttributeError: type object 'Factura' has no attribute '__tasa'

Python protege estos atributos cambiando su nombre internamente. A sus nombres agrega el nombre de la clase:

objeto._NombreClase__NombreAtributo.

print(compra1._Factura__tasa)

# 19

Propiedades (properties)


Cuando se trabajan con clases es recomendable crear atributos ocultos y utilizar métodos específicos para acceder a los mismos para establecer, obtener o borrar la información:

class Empleado:
    def __init__(self, nombre, salario):
        self.__nombre = nombre
        self.__salario = salario
        
    def getnombre(self):
        return self.__nombre

    def getsalario(self):
        return self.__salario
  
    def setnombre(self, nombre):
        self.__nombre = nombre

    def setsalario(self, salario):
        self.__salario = salario

    def delnombre(self):
        del self.__nombre

    def delsalario(self):
        del self.__salario

empleado1 = Empleado("Francisco", 30000)
print(empleado1.getnombre())
empleado1.setnombre("Francisco José")
print(empleado1.getnombre(), ",", empleado1.getsalario())

Estos métodos son útiles principalmente para los atributos más importantes de un objeto, generalmente aquellos que necesitan ser accedidos desde otros objetos.

Pero hay otra alternativa al uso de estos métodos basada en las propiedades Python que simplifica la tarea. Las propiedades en Python son un tipo especial de atributo a los que se accede a través de llamadas a métodos. Con ello, es posible ocultar los métodos "get", "set" y "del" de manera que sólo es posible acceder mediante estas propiedades por ser públicas.

No es obligatorio definir métodos "get", "set" y "del" para todas las propiedades. Es recomendable sólo para aquellos atributos en los que sea necesario algún tipo de validación anterior a establecer, obtener o borrar un valor. Para que una propiedad sea sólo de lectura hay que omitir las llamadas a los métodos "set" y "del".

class Empleado():
    def __init__(self, nombre, salario):
        self.__nombre = nombre
        self.__salario = salario
        
    def __getnombre(self):
        return self.__nombre

    def __getsalario(self):
        return self.__salario
  
    def __setnombre(self, nombre):
        self.__nombre = nombre

    def __setsalario(self, salario):
        self.__salario = salario

    def __delnombre(self):
        del self.__nombre

    def __delsalario(self):
        del self.__salario
    
    nombre = property(fget = __getnombre, 
                      fset = __setnombre, 
                      fdel = __delnombre, 
                      doc = "Soy la propiedad 'nombre'")
    salario = property(fget = __getsalario, 
                       doc = "Soy la propiedad 'salario'")

empleado1 = Empleado("Francisco José", 30000)
empleado1.nombre = "Rosa"  # Realiza una llamada al método "fset"
print(empleado1.nombre, 
      empleado1.salario)  # Realiza una llamada al método "fget"

La siguiente asignación no se puede realizar porque la propiedad se ha definido como de sólo lectura. Produce una excepción del tipo AttributeError: can't set attribute

empleado1.salario = 33000

Para mostrar la documentación del objeto:

help(empleado1)

Help on Empleado in module __main__ object:

class Empleado(builtins.object)
| Methods defined here:
|
| __init__(self, nombre, salario)
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| nombre
| Soy la propiedad 'nombre'
|
| salario
| Soy la propiedad 'salario'



Orden de Resolución de Métodos (MRO). El atributo especial __mro__


Es importante conocer cómo funciona la herencia en Python cuando existe una jerarquía con varios niveles de clases que pueden tener definidos métodos que utilizan el mismo nombre.

En el siguiente ejemplo se define un primer nivel de clases con una clase llamada Clase_A. A continuación, en un segundo nivel se definen dos clases más (Clase_A1 y Clase_A2) que heredan de la primera. Y en el tercer y último nivel, se define la clase Clase_X que hereda de las dos clases de segundo nivel.

Teniendo en cuenta que en las clases mencionadas hay métodos con el mismo nombre, vamos a mostrar cómo calcula Python el orden de resolución de métodos.

class Clase_A(object):
    def metodo1(self):
        print("Clase_A.metodo1()")
        
    def metodo3(self):
        print("Clase_A.metodo3()")
        
    def metodo4(self):
        print("Clase_A.metodo4()")

class Clase_A1(Clase_A):
    def metodo1(self):
        print("Clase_A1.metodo1()")

    def metodo2(self):
        print("Clase_A1.metodo2()")

class Clase_A2(Clase_A):
    def metodo1(self):
        print("Clase_A2.metodo1()")

    def metodo3(self):
        print("Clase_A2.metodo3()")

class Clase_X(Clase_A1, Clase_A2):
    def metodo1(self):
        print("Clase_X.metodo1()")

objeto1 = Clase_X()  # Creación de una instancia (objeto) de Clase_X
objeto1.metodo1()  # Clase_X.metodo1()
objeto1.metodo2()  # Clase_A1.metodo2()
objeto1.metodo3()  # Clase_A2.metodo3()
objeto1.metodo4()  # Clase_A.metodo4()

En el ejemplo se crea el objeto objeto1 de la Clase_X y después se llama al método objeto1.metodo1(). Como dicho método existe en la propia Clase_X ese será al que se llame, con independencia de que exista en otra clase.

Como puede comprobarse el método metodo1() existe en todas las clases. Si no existiera en la Clase_X se hubiera llamado al de la clase Clase_A1 que tiene mayor prioridad, primero, porque se encuentra en el nivel inmediatamente anterior y, segundo, porque esa clase es nombrada antes que Clase_A2 en la definición de Clase_X.

A continuación, se invoca al método objeto1.metodo2() que no existe en la clase Clase_X pero si existe en la clase Clase_A1. Como dicho método no existe en otro lugar, ese será el llamado.

Después, se invoca al método objeto1.metodo3() que existe tanto en Clase_A2 como en Clase_A. Como la Clase_A2 se encuentra en el nivel inmediatamente superior (con respecto a la Clase_X) ese será el llamado.

Por último, se llama al método objeto1.metodo4() que no existe en ninguna clase del nivel inmediatamente superior. Como dicho método está presente en la clase Clase_A, ese será el invocado.

La situación puede complicarse si el número de clases aumenta, si hay más niveles y además las clases tienen ancestros diferentes.

En definitiva, dentro de una jerarquía de clases la sobrecarga se resuelve de abajo a arriba y de izquierda a derecha. Si una clase hereda de varias clases se considerará también el orden en que fueron declaradas en la propia definición, es decir, no es igual definir la clase así [class Clase_X(Clase_A1, Clase_A2)] que de esta forma: [class Clase_X(Clase_A2, Clase_A1)].

Para calcular el orden de resolución Python utiliza el método MRO (Method Resolution Order) basado en un algoritmo llamado C3.

El cálculo realizado se puede consultar accediendo al atributo especial __mro__, que devuelve una tupla con las clases por su orden de resolución de métodos (MRO).

print(Clase_X.__mro__)  

# (class '__main__.Clase_X', class '__main__.Clase_A1', 
#  class '__main__.Clase_A2', class '__main__.Clase_A', class 'object')

Si cambiamos en la definición de la clase Clase_X el orden de las clases [class Clase_X(Clase_A2, Clase_A1)] el resultado de mostrar el atributo __mro__ será:

print(Clase_X.__mro__)  
# (class '__main__.Clase_X', class '__main__.Clase_A2', 
#  class '__main__.Clase_A1', class '__main__.Clase_A', class 'object')

La función super()


La función super() se utiliza para llamar a métodos definidos en alguna de las clases de las que se hereda sin nombrarla/s explícitamente, teniendo en cuenta el orden de resolución de métodos (MRO). No hay problemas cuando se hereda de sólo una clase, pero si la jerarquía de clases es extensa podemos obtener resultados inesperados si no se tiene un amplio conocimiento de todas las clases y de sus vínculos.

En el siguiente ejemplo se definen las clases Clase_I y Clase_II con dos métodos cada una, siendo uno de ellos el método constructor o método __init__.

A continuación, se definen las clases Clase_III y Clase_IV que heredan sus métodos y atributos de las clases Clase_I y Clase_II, pero en cada caso se han establecido con un orden distinto en la definición.

Después, para probar el funcionamiento de la función super() se instancian dos objetos de la Clase_III y Clase_IV, se invocan métodos y se acceden a los atributos. En el propio código se analizan los resultados obtenidos.

class Clase_I(object):
    def __init__(self):
        self.var1 = 1
        print('Clase_I.__init__')
        
    def metodo1(self):
        self.var2 = 1
        print('Clase_I.metodo1()')

class Clase_II(object):
    def __init__(self):
        self.var1 = 2
        print('Clase_II.__init__')
        
    def metodo1(self):
        self.var2 = 2
        print('Clase_II.metodo1()')
        
class Clase_III(Clase_I, Clase_II):
    def __init__(self):
        self.var1 = 3
        print('Clase_III.__init__', end = ', ')
        super().__init__()
        
    def metodo1(self):
        print('Clase_III.metodo1()', end = ', ')
        super().metodo1()
        self.var2 = 3

class Clase_IV(Clase_II, Clase_I):
    def __init__(self):
        self.var1 = 4
        print('Clase_IV.__init__', end = ', ')
        super().__init__()
        
    def metodo1(self):
        print('Clase_IV.metodo1()', end = ', ')
        super().metodo1()
        self.var2 = 4

# Al crear objeto1 y objeto2 en el método __init__ se 
# invoca también el método __init__ de su clase superior

objeto1 = Clase_III()  # Clase_III.__init__, Clase_I.__init__
objeto2 = Clase_IV()  # Clase_IV.__init__, Clase_II.__init__

# El atributo especial __mro__ retorna una tupla
# con las clases ordenadas de izquierda a derecha
# que indican la prioridad en la herencia.
# Mientras en la Clase_III tiene mayor prioridad en 
# la herencia la Clase_I que la Clase_II; en la Clase_IV
# es al revés

print(Clase_III.__mro__)
# (class '__main__.Clase_III', class '__main__.Clase_I', 
#  class '__main__.Clase_II', class 'object')
 
print(Clase_IV.__mro__)  
# (class '__main__.Clase_IV', class '__main__.Clase_II', 
#  class '__main__.Clase_I', class 'object')

# Al llamar al metodo1 de objeto1 y objeto2 se invoca 
# también el equivalente de su clase superior

objeto1.metodo1()  # Clase_III.metodo1(), Clase_I.metodo1()
objeto2.metodo1()  # Clase_IV.metodo1(), Clase_II.metodo1()

# Al acceder a la variable var1 de objeto1 y objeto2
# se obtiene el valor que tiene en la clase superior
# porque en el método __init__ de su clase, DESPUÉS de
# la asignación, se invoca con la función super() al 
# método __init__ de la clase superior donde se realiza
# una asignación a la misma variable.

print(objeto1.var1)  # 1
print(objeto2.var1)  # 2

# Al acceder a la variable var2 de objeto1 y objeto2
# se obtiene el valor que tiene en su clase 
# porque aunque en el método metodo1() de su clase 
# se invoca con la función super() a su equivalente de
# la clase superior, la invocación se realiza ANTES
# de la asignación a dicha variable.

print(objeto1.var2) # 3
print(objeto2.var2) # 4



Ir al índice del tutoríal de Python

Programación Orientada a Objetos (II)



Funciones para atributos: getattr(), hasattr(), setattr() y delattr()


getattr()

La función getattr() se utiliza para acceder al valor del atributo de un objeto Si un atributo no existe retorna el valor del tercer argumento (es opcional).

nota_alumno2 = getattr(alumno2, 'nota', 0)
edad_alumno2 = getattr(alumno2, 'edad', 0) 
suma_notas = getattr(Alumno, 'sumanotas')
print(nota_alumno2)  # 6
print(edad_alumno2)  # 0
print(suma_notas)  # 15

hasattr()

La función hasattr() devuelve True o False dependiendo si existe o no el atributo indicado.

if not hasattr(alumno2, 'edad'):
    print("El atributo 'edad' no existe")

setattr()

Se utiliza para asignar un valor a un atributo. Si el atributo no existe entonces será creado.

setattr(alumno2, 'edad', 18)
print(alumno2)  # 18

delattr()

La función delattr() es para borrar el atributo de un objeto. Si el atributo no existe se producirá una excepción del tipo AttributeError.

delattr(alumno2, 'edad')
delattr(alumno2, 'curso')

Atributos de clase (Built-In)


Todas las clases Python incorporan los siguientes atributos especiales:

__dict__

Devuelve un diccionario que contiene el espacio de nombres de la clase (o de un objeto instanciado).

print(Alumno.__dict__)

{'mostrarSumaNotas': function 0xb7071854="" alumno.mostrarsumanotas="" at="", 'mostrarNotaMedia': function 0xb707189c="" alumno.mostrarnotamedia="" at="", '__doc__': 'Clase para alumnos', '__init__': function 0xb70717c4="" alumno.__init__="" at="", '__weakref__': attribute lumno="" objects="" of="" weakref__="", '__dict__': attribute dict__="" lumno="" objects="" of="", 'mostrarNumAlumnos': function 0xb7071734="" alumno.mostrarnumalumnos="" at="", 'sumanotas': 14, '__module__': '__main__', 'numalumnos': 2, 'mostrarNombreNota': function 0xb707177c="" alumno.mostrarnombrenota="" at=""}

 
print(alumno1.__dict__)

# {'nombre': 'Carmen', 'nota': 8}

__name__

Devuelve el nombre de la clase.

print(Alumno.__name__)

# Alumno

__doc__

Devuelve la cadena de documentación de la clase o None si no se ha definido.

print(Alumno.__doc__)

# Clase para alumnos

__module__

Devuelve el nombre del módulo donde se encuentra la clase definida.

print(Alumno.__module__)  # __main__

from datetime import date
print(date.__module__)  

# datetime

__bases__

Devuelve una tupla (posiblemente vacía) que contiene las clases base, en orden de aparición.

print(Alumno.__bases__)  

# (class object="",)

A continuación, se define la clase Becario a partir de la clase Alumno. La clase Becario hereda los atributos y métodos de la clase Alumno. Al acceder al atributo __bases__ de la nueva clase retorna la referencia a la clase padre Alumno.

class Becario(Alumno):
    pass

print(Becario.__bases__)  

# (class main__.alumno="",)

Destrucción de objetos (Recolección de basura)


Python elimina objetos que no son necesarios (del tipo Built-In o instancias de clase) automáticamente para liberar espacio de memoria. El proceso por el cual Python reclama periódicamente bloques de memoria que ya no están en uso se denomina Recolección de Basura.

La Recolección de Basura de Python se ejecuta durante la ejecución de un programa y se activa cuando el contador de referencia de un objeto llega al valor cero. Este contador va cambiando su valor en función del uso que se haga del objeto. Si se asigna un nuevo nombre o se utiliza en una lista, tupla o diccionario el contador irá en aumento y si se borra el objeto con del, su referencia es reasignada o bien se queda fuera de ámbito, va disminuyendo. En el momento que se alcanza el valor cero el recolector comienza con la tarea de "rescate".

Ejemplo:

pizza1 = "Margarita"  # Crear objeto margarita
pizza2 = pizza1  # Incrementa el contador de referencia de margarita

cena['lunes'] = pizza2  # Incrementa contador de referencia margarita

del pizza1  # Decrementa el contador de referencia de margarita
pizza2 = "Cuatro Estaciones"  # Decrementa contador ref. margarita
cena['lunes'] = pizza2  # Decrementa contador referencia margarita

El trabajo del recolector pasa totalmente desapercibido para un usuario. Actuará automáticamente cuando detecte instancias huérfanas, recuperando su espacio de memoria.

No obstante, una clase puede incluir el método __del__(), llamado destructor, que se invoca cuando una instancia está a punto de ser destruida con del, o si no ha sido destruida con esta sentencia, cuando el programa finalice su ejecución.

Este método especial puede ser utilizado para limpiar aquellos recursos de memoria no usados por una instancia.

Ejemplo:

class Robot:
    'Clase Robot'
    def __init__(self, nombre):
        self.nombre = nombre
        
    def saludo(self):
        print("Hola, mi nombre es", self.nombre)

    def __del__(self):
        print("Se han terminado mis baterias. Me voy a dormir.")

robot1 = Robot("C7PQ")
robot1.saludo()  # Hola, mi nombre es C7PQ
print(id(robot1))  # Muestra el ID del objeto, Ejem.: 3070861004
del robot1  # Se han terminado mis baterias. Me voy a dormir.

Si intentamos mostrar de nuevo el ID del objeto, llamar al método saludo() o acceder al atributo nombre se producirá el siguiente error:

print(id(robot1))
robot1.saludo()
print(robot1.nombre)

NameError: name 'robot1' is not defined

Es recomendable definir las clases en un archivo separado. Más adelante, cuando se deseen utilizar las clases en un programa se importará el archivo con la sentencia import.

Herencia


La herencia es una de las características más importantes de la Programación Orientada a Objetos. Consiste en la posibilidad de crear una nueva clase a partir de una o más clases existentes, heredando de ellas sus atributos y métodos que podrán utilizarse como si estuvieran definidos en la clase hija.

Las clases derivadas se declaran como cualquier clase con la diferencia de incluir después de su nombre el nombre de la clase superior (entre paréntesis) de la que heredará sus características:

class NombreSubClase (NombreClaseSuperior):
    'Cadena de documentación opcional'
    Declaración de atributos y métodos...

Ejemplo:

class volumen:
    'Clase para controlar volumen de un reproductor multimedia'
    def __init__(self):  # método constructor de objeto. Activa volumen
        self.nivel = 3  # sitúa el nivel de volumen en 3
        print('nivel', self.__class__.__name__, self.nivel)

    def subir(self):  # método para subir el nivel de volumen de 1 en 1
        self.nivel += 1
        if self.nivel > 10:  # al intentar sobrepasar el nivel 10
            self.nivel = 10  # el nivel permanece en 10
        
        print('nivel', self.__class__.__name__, self.nivel)

    def bajar(self):  # método para bajar el nivel de 1 en 1
        self.nivel -= 1
        if self.nivel < 0:  # al intentar bajar por debajo del nivel 0
            self.nivel = 0  # el nivel permanece en 0
        
        print('nivel', self.__class__.__name__, self.nivel)

    def silenciar(self):  # método para silenciar
        self.nivel = 0  # el nivel se sitúa en el 0
        print('nivel', self.__class__.__name__, self.nivel)

class graves(volumen):  # crea clase graves a partir de clase volumen
    pass

ControlVolumen = volumen()  # crea objeto y activa el volumen en 3
ControlVolumen.subir()  # sube el volumen del nivel 3 al 4
ControlVolumen.bajar()  # baja el volumen del nivel 4 al 3
ControlVolumen.silenciar()  # silencia volumen bajando del 3 al 0
ControlGraves = graves()  # crea objeto control graves, activa nivel 3
ControlGraves.subir()  # sube el nivel de graves del 3 nivel al 4
del ControlVolumen  # borra el objeto
del ControlGraves  # borra el objeto

Herencia múltiple


La herencia múltiple se refiere a la posibilidad de crear una clase a partir de múltiples clases superiores. Es importante nombrar adecuadamente los atributos y los métodos en cada clase para no crear conflictos:

Ejemplo:

class Telefono:
    "Clase teléfono"
    def __init__(self):
        pass
    def telefonear(self):
        print('llamando')
    def colgar(self):
        print('colgando')        

class Camara:
    "Clase camara fotográfica"
    def __init__(self):
        pass
    def fotografiar(self):
        print('fotografiando')        

class Reproductor:
    "Clase Reproductor Mp3"
    def __init__(self):
        pass
    def reproducirmp3(self):
        print('reproduciendo mp3')                  
    def reproducirvideo(self):
        print('reproduciendo video')                  

class Movil(Telefono, Camara, Reproductor):
    def __del__(self):
        print('Móvil apagado')

movil1 = Movil()
print(dir(movil1))
movil1.reproducirmp3()
movil1.telefonear()
movil1.fotografiar()
del movil1

Salida:

['__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'colgar', 'fotografiar', 'reproducirmp3', 'reproducirvideo', 'telefonear']

reproduciendo mp3
llamando
fotografiando
Móvil apagado

Funciones issubclass() y isinstance()


La función issubclass(SubClase, ClaseSup) se utiliza para comprobar si una clase (SubClase) es hija de otra superior (ClaseSup), devolviendo True o False según sea el caso.

print(issubclass(Movil, Telefono))  # True
print(issubclass(Movil, Reproductor))  # True

La función booleana isinstance(Objeto, Clase) se utiliza para comprobar si un objeto pertenece a una clase o clase superior.

movil2 = Movil()
print(isinstance(movil2, Movil))  # True
print(isinstance(movil2, Camara))  # True




Programación Orientada a Objetos (I)



Introducción


Un Programa Orientado a Objetos (POO) se basa en una agrupación de objetos de distintas clases que interactúan entre sí y que, en conjunto, consiguen que un programa cumpla su propósito.

En este paradigma de programación se intenta emular el funcionamiento de los objetos que nos rodean en la vida real.

En Python cualquier elemento del lenguaje pertenece a una clase y todas las clases tienen el mismo rango y se utilizan del mismo modo.

A continuación, se declaran varios objetos Python y con la función type() se muestra a qué clase pertenecen cada uno:

lenguaje = "Python"
print(type(lenguaje))  # class str=""

def mitad(x):
    return x/2
 
print(type(mitad))  # class function=""

valor = mitad(25)
print(type(valor))  # class float=""

import os
print(type(os))  # class module=""

lista = [1, 2, 3, 4, 5]
print(type(lista))  # class list=""

Clases, atríbutos y métodos


Las clases en este contexto permiten definir los atributos y el comportamiento, mediante métodos, de los objetos de un programa. Una clase es una especie de plantilla o prototipo que se utiliza para crear instancias individuales del mismo tipo de objeto.

Los atributos definen las características propias del objeto y modifican su estado. Son datos asociados a las clases y a los objetos creados a partir de ellas.

De ello, se deducen los dos tipos de atributos o de variables existentes: variables de clase y variables de instancia (objetos).

Los métodos son bloques de código (o funciones) de una clase que se utilizan para definir el comportamiento de los objetos.

Tanto para acceder a los atributos como para llamar a los métodos se utiliza el método denominado de notación de punto que se basa en escribir el nombre del objeto o de la clase seguido de un punto y el nombre del atributo o del método con los argumentos que procedan: clase.atributo, objeto.atributo, objeto.método([argumentos]).

Un ejemplo característico de objeto Python donde se identifican fácilmente los métodos son las listas. Una lista es un objeto que permite contener una colección o secuencia de datos. Los datos de una lista deben ir separados por comas (,) y todo el conjunto entre corchetes. Una lista es una estructura mutable porque no sólo se puede acceder a los datos, además, es posible agregar nuevos elementos o suprimir aquellos que no sean necesarios. La clase lista (List) incorpora varios métodos para facilitar este trabajo:

lista = ['c', 'a', 'b']  # Declara lista con tres elementos
lista.append('d')  # Agrega elemento al final de lista con append()
lista.pop()  # Borra último elemento de lista con método pop()
lista.sort()  # Ordena la lista con el método sort()
print(lista)  # ['a', 'b', 'c']

Características de la Programación Orientada a Objeto


Características que definen a este modelo de programación:

Abstracción

Se refiere a que un elemento pueda aislarse del resto de elementos y de su contexto para centrar el interés en lo qué hace y no en cómo lo hace (caja negra). 

Modularidad

Es la capacidad de dividir una aplicación en partes más pequeñas independientes y reutilizables llamadas módulos. 

Encapsulación

Consiste en reunir todos los elementos posibles de una entidad al mismo nivel de abstracción para aumentar la cohesión, contando con la posibilidad de ocultar los atributos de un objeto (en Python, sólo se ocultan en apariencia).

Herencia

se refiere a que una clase pueda heredar las características de una clase superior para obtener objetos similares. Se heredan tanto los atributos como los métodos. Éstos últimos pueden sobrescribirse para adaptarlos a las necesidades de la nueva clase. A la posibilidad de heredar atributos y métodos de varias clases se denomina Herencia Múltiple.

Polimorfismo

Alude a la posibilidad de identificar de la misma forma comportamientos similares asociados a objetos distintos. La idea es que se sigan siempre las mismas pautas aunque los objetos y los resultados sean otros.

Variables de Clases y Variables de Instancias


En lenguajes que crean objetos a partir de clases, un objeto es una instancia de una clase. Y de una misma clase se pueden mantener activas en un programa más de una instancia al mismo tiempo.

Una variable de clase es compartida por todas las instancias de una clase. Se definen dentro de la clase (después del encabezado de la clase) pero nunca dentro de un método. Este tipo de variables no se utilizan con tanta frecuencia como las variables de instancia.

Una variable de instancia se define dentro de un método y pertenece a un objeto determinado de la clase instanciada.

Crear clases


Una clase consta de dos partes: un encabezado que comienza con el término class seguido del nombre de la clase (en singular) y dos puntos (:) y un cuerpo donde se declaran los atributos y los métodos:

class NombreClase:
    'Texto para documentar la clase (opcional)'
    varclase1 = "variable de clase1"

    def nombremetodo1(self, var1):
        self.var1 = var1

    def nombremetodo2(self):
        self.var1 += 1

La documentación de una clase debe situarse después del encabezado y justo antes del lugar donde se declaren las variables y los métodos de la clase.

Desde cualquier lugar de un programa se puede acceder a la cadena de documentación de una clase accediendo al atributo especial: NombreClase.__doc__ 

Todo lo que se incluye en una clase es opcional. De hecho, la clase más elemental aunque no tenga mucha utilidad puede estar vacía:

class Clase:
    pass

La declaración pass indica que no se ejecutará ningún código. Sin embargo, esta clase una vez definida permite que se instancien objetos de ella e incluso es posible realizar algunas operaciones elementales.

objeto1 = Clase()  # Crea objeto1 de clase Clase
objeto2 = Clase()  # Crea objeto2 de clase Clase
print(objeto1 == objeto2)  # Retorna False...
# Los objetos aunque sean de la misma clase son diferentes.

En el siguiente ejemplo se define una clase mucho más completa:

class Alumno:
    'Clase para alumnos'
    numalumnos = 0
    sumanotas = 0

    def __init__(self, nombre, nota):
        self.nombre = nombre
        self.nota = nota
        Alumno.numalumnos += 1
        Alumno.sumanotas += nota

    def mostrarNombreNota(self):
        return(self.nombre, self.nota)

    def mostrarNumAlumnos(self):
        return(Alumno.numalumnos)

    def mostrarSumaNotas(self):
        return(Alumno.sumanotas)

    def mostrarNotaMedia(self):
        if Alumno.numalumnos > 0:
            return(Alumno.sumanotas/Alumno.numalumnos)
        else:
            return("Sin alumnos")

La clase Alumno consta de dos variables de clase (Alumno.numalumnos y Alumno.sumanotas) que son accesibles desde los métodos de la clase. Además, sus valores son compartidos por todas las instancias que existan de esta clase.

A continuación, se declaran varios métodos (funciones) que incluyen como primer argumento a self que contiene la referencia del objeto especifico que llama al método en un momento dado. Como su valor es implícito cuando se llama a un método no es necesario pasar este argumento.

El método __init__() es especial porque se ejecuta automáticamente cada vez que se crea una nuevo objeto. Este método, que es opcional, se llama constructor y se suele utilizar para inicializar las variables de las instancias (en este caso para inicializar las variables self.nombre y self.nota).

El resto de métodos se utilizan para acceder y mostrar el valor de las variables de clase y de instancia. Por último, el método mostrarNotaMedia() realiza un cálculo y después muestra su resultado.

Crear objetos (instancias) de una clase


Para crear instancias de una clase se llama a la clase por su propio nombre pasando los argumentos que requiera el método constructor __init__ si existe.

alumno1 = Alumno("Maria", 8)
alumno2 = Alumno("Carlos", 6)

Todos los argumentos se pasan escribiéndolos entre paréntesis y separados por comas (","). El primer argumento self se omite porque su valor es una referencia al objeto y es implícito para todos los métodos.

Accediendo a los atributos y llamando a los métodos


Para acceder a la variable de un objeto se indica el nombre del objeto, seguido de un punto y el nombre de la variable:

print(alumno1.nombre)  # María
print(alumno1.nota)  # 8

Para modificar la variable de un objeto se utiliza la misma notación para referirse al atributo y después del signo igual (=) se indica la nueva asignación:

alumno1.nombre = "Carmela"

Para acceder a las variables de la clase se sigue la misma notación pero en vez de indicar el nombre del objeto se indica el nombre de la clase instanciada.

print(Alumno.numalumnos)  # 2
print(Alumno.sumanotas)  # 14

Para llamar a un método se indica el nombre de objeto, seguido de un punto y el nombre del método. Si se requieren varios argumentos se pasan escribiéndolos entre paréntesis, separados por comas (","). Si no es necesario pasar argumentos se añaden los paréntesis vacíos "()" al nombre del método.

print(alumno1.mostrarNombreNota())  # ('Carmen', 8)
print(alumno2.mostrarNombreNota())  # ('Carlos', 6)

Para suprimir un atributo:

del alumno1.nombre

Si a continuación, se intenta acceder al valor del atributo borrado o se llama a algún método que lo utilice, se producirá la siguiente excepción:

print(alumno1.mostrarNombreNota())

AttributeError: 'Alumno' object has no attribute 'nombre'

Pare crear nuevamente el atributo realizar una nueva asignación:

alumno1.nombre = "Carmen"


Continúa en: Programación Orientada a Objetos (II)