Python Orientado a Objetos

La Programación Orientada a Objetos (POO u OOP según sus siglas en inglés) es un paradigma de programación en el que los conceptos del mundo real relevantes para nuestro problema se modelan a través de clases y objetos, y en el que nuestro programa consiste en una serie de interacciones entre estos objetos.

Clases y objetos

Para entender este paradigma primero tenemos que comprender qué es una clase y qué es un objeto. Un objeto es una entidad que agrupa un estado y una funcionalidad relacionadas. El estado del objeto se define a través de variables llamadas atributos, mientras que la funcionalidad se modela a través de funciones a las que se les conoce con el nombre de métodos del objeto.

Un ejemplo de objeto podría ser un coche, en el que tendríamos atributos como la marca, el número de puertas o el tipo de carburante y métodos como arrancar y parar. O bien cualquier otra combinación de atributos y métodos según lo que fuera relevante para nuestro programa.

Una clase, por otro lado, no es más que una plantilla genérica a partir de la cuál instanciar los objetos; plantilla que es la que define qué atributos y métodos tendrán los objetos de esa clase.

Volviendo a nuestro ejemplo: en el mundo real existe un conjunto de objetos a los que llamamos coches y que tienen un conjunto de atributos comunes y un comportamiento común, esto es a lo que llamamos clase. Sin embargo, mi coche no es igual que el coche de mi vecino, y aunque pertenecen a la misma clase de objetos, son objetos distintos.

En Python las clases se definen mediante la palabra clave class seguida del nombre de la clase, dos puntos (:) y a continuación, indentado, el cuerpo de la clase. Como en el caso de las funciones, si la primera línea del cuerpo se trata de una cadena de texto, esta será la cadena de documentación de la clase o docstring.

En Python, una clase se define con la instrucción class seguida de un nombre genérico para el objeto.

class Objeto:
    pass
 
class Antena:
    pass
 
class Pelo:
    pass
 
class Ojo:
    pass
 
PEP 8: clases El nombre de las clases se define en singular, utilizando CamelCase.
 

Propiedades

Las propiedades, como hemos visto antes, son las características intrínsecas del objeto. Éstas, se representan a modo de variables, solo que técnicamente, pasan a denominarse propiedades:

class Antena(): 
    color = "" 
    longitud = "" 
 
class Pelo(): 
    color = "" 
    textura = "" 
 
class Ojo(): 
    forma = "" 
    color = "" 
    tamanio = ""
 
class Objeto(): 
    color = "" 
    tamanio = "" 
    aspecto = "" 
    antenas = Antena() # propiedad compuesta por el objeto objeto Antena
    ojos = Ojo()       # propiedad compuesta por el objeto objeto Ojo
    pelos = Pelo()     # propiedad compuesta por el objeto objeto Pelo
 
 PEP 8: propiedades Las propiedades se definen de la misma forma que las variables (aplican las mismas reglas de estilo).
 

Métodos

Los métodos son funciones (como las que vimos en el capítulo anterior), solo que técnicamente se denominan métodos, y representan acciones propias que puede realizar el objeto (y no otro):

class Objeto(): 
    color = "verde" 
    tamanio = "grande" 
    aspecto = "feo" 
    antenas = Antena() 
    ojos = Ojo() 
    pelos = Pelo() 
 
    def flotar(self): 
        pass
 
Nota Notar que el primer parámetro de un método, siempre debe ser self.
 

Herencia: característica principal de la POO

Algunos objetos comparten las mismas propiedades y métodos que otro objeto, y además agregan nuevas propiedades y métodos. A esto se lo denomina herencia: una clase que hereda de otra. Vale aclarar, que en Python, cuando una clase no hereda de ninguna otra, debe hacerse heredar de object, que es la clase principal de Python, que define un objeto.

class Antena(object): 
    color = "" 
    longitud = ""
 
class Pelo(object): 
    color = "" 
    textura = ""
 
class Ojo(object): 
    forma = "" 
    color = "" 
    tamanio = ""
 
class Objeto(object): 
    color = "" 
    tamanio = "" 
    aspecto = "" 
    antenas = Antena() 
    ojos = Ojo() 
    pelos = Pelo() 
 
    def flotar(self):
        pass
 
class Dedo(object): 
    longitud = "" 
    forma = "" 
    color = ""
 
class Pie(object): 
    forma = "" 
    color = "" 
    dedos = Dedo() 
 
# NuevoObjeto sí hereda de otra clase: Objeto
class NuevoObjeto(Objeto): 
    pie = Pie() 
 
    def saltar(self): 
        pass
 

Accediendo a los métodos y propiedades de un objeto

Una vez creado un objeto, es decir, una vez hecha la instancia de clase, es posible acceder a su métodos y propiedades. Para ello, Python utiliza una sintaxis muy simple: el nombre del objeto, seguido de punto y la propiedad o método al cuál se desea acceder:

objeto = MiClase() 
print objeto.propiedad 
objeto.otra_propiedad = "Nuevo valor" 
variable = objeto.metodo()
print variable
print objeto.otro_metodo()
 

Herencia Múltiple

Python soporta la herencia múltiple, dle mismo modo que C++. Otros lenguajes cómo Java y Ruby no la soportan, pero sí que implementan técnicas para conseguir la misma funcionalidad. En el caso de Java contamos con las clases abstractas y las interfaces, y en Ruby tenemos los mixins.
La herencia múltiple es similar en comportamiento a la sencilla, con la diferencia que una clase hija tiene uno o más clases padre. En Python, basta con separar con comas los nombres de las clases en la definición de la misma. Vamos a pensar en un ejemplo de la vida real para implementar la herencia múltiple. Por ejemplo, una clase genérica sería Persona, otra Personal y la hija sería Mantenimiento. De está manera, una persona que trabajara en una empresa determinada cómo personal de mantenimiento, podría representarse a través de una clase de la siguiente manera:

class Mantenimiento(Persona, Personal):
 pass
 
De está forma, desde la clase Mantenimiento, tendríamos acceso a todos los atributos y métodos declarados, tanto en Persona, cómo en Personal.

La herencia múltiple presenta el conocido problema del diamante. Esté problema surge cuándo dos clases heredan de otra tercera y, además una cuarta clase tiene cómo padre a las dos últimas. La primera clase padre es llamada A y las clases B y C heredan de ella, a su vez la clase D tiene cómo padres a B y C. En está situación, si una instancia de la clase D llama a un método de la clase A, ¿lo heredará desde la clase B o desde la clase C?. Cada lenguaje de programación utiliza un algoritmo para tomar está decisión. En el caso particular de Python, se toma cómo referencia que todas las clases descienden de la clase padre object. Además, se crea una lista de clases que se buscan de derecha a izquierda y de bajo arriba, posteriormente se eliminan todas las apariciones de una clase repetida menos la última. De está manera queda establecido un orden.

Debido a las ambigüedades que pueden surgir de la utilización de la herencia múltiple, son muchos los desarrolladores que deciden emplearla lo mínimo posible, debido a que, dependiendo de la complejidad del diagrama de herencia, puede ser muy complicado establecer su orden y se pueden producir errores no deseados en tiempo de ejecución. Por otro lado, si mantenemos una relación sencilla, la herencia múltiple es un útil aliado a la hora de representar objetos y situaciones de la vida real.
 

 

Polimorfismo

El concepto de polimorfismo (del griego muchas formas) implica que si en una porción de código se invoca un determinado método de un objeto, podrán obtenerse distintos resultados según la clase del objeto. Esto se debe a que distintos objetos pueden tener un método con un mismo nombre, pero que realice distintas operaciones.

En las unidades anteriores, varias veces utilizamos las posibilidades provistas por el polimorfismo, sin haberle puesto este nombre.

Se vio, por ejemplo, que es posible recorrer cualquier tipo de secuencia (ya sea una lista, una tupla, un diccionario, un archivo o cualquier otro tipo de secuencia) utilizando la misma estructura de código (for elemento in secuencia).

De la misma forma, hemos utilizado funciones que podían trabajar con los distintos tipos numéricos sin hacer distinción sobre de qué tipo de número se trataba (entero, real, largo o complejo).

Por otro lado, en la unidad anterior se vio también que al construir una clase, es posible incluir el método __str__ para que cuando se quiera imprimir el objeto se lo haga de la forma deseada; así como una gran variedad de otros métodos especiales, que permiten que operadores comunes sean utilizados sobre distintos tipos de objetos.

Un ejemplo de polimorfismo

Sea la clase Punto, una clase que representa a un punto en el plano. Es posible definir también una clase Punto3D, que represente un punto en el espacio. Esta nueva clase contendrá los mismos métodos que se vieron para Punto, pero para tres coordenadas.

Si a ambas clases le agregamos un método para multiplicar por un escalar (__mul__(self, escalar)), podríamos tener la siguiente función polimórfica:

def obtener_versor(punto):
    norma = punto.norma()
    return punto * (1.0 / norma)
 
Esta función devolverá un versor de dos dimensiones o de tres dimensiones, según a qué clase pertenezca la variable punto.

Advertencia A veces puede suceder que una función polimórfica imponga alguna restricción sobre los tipos de los parámetros sobre los que opera. En el ejemplo anterior, el objeto punto debe tener el método norma y la posibilidad de multiplicarlo por un escalar.

Otro ejemplo que ya hemos visto, utilizando secuencias, es usar un diccionario para contar la frecuencia de aparación de elementos dentro de una secuencia cualquiera.

def frecuencias(secuencia):
    """ Calcula las frecuencias de aparición de los elementos de
        la secuencia recibida.
        Devuelve un diccionario con elementos: {valor: frecuencia}
    """
    # crea un diccionario vacío
    frec = dict()
    # recorre la secuencia
    for elemento in secuencia:
        frec[elemento] = frec.get(elemento, 0) + 1
    return frec
 
Vemos que el parámetro secuencia puede ser de cualquier tipo que se encuentre dentro de la "familia" de las secuencias. En cambio, si llamamos a la función con un entero se levanta una excepción.

>>> frecuencias(["peras", "manzanas", "peras", "manzanas", "uvas"])
{'uvas': 1, 'peras': 2, 'manzanas': 2}
>>> frecuencias((1,3,4,2,3,1))
{1: 2, 2: 1, 3: 2, 4: 1}
>>> frecuencias("Una frase")
{'a': 2, ' ': 1, 'e': 1, 'f': 1, 'n': 1, 's': 1, 'r': 1, 'U': 1}
>>> ran = xrange(3, 10, 2)
>>> frecuencias(ran)
{9: 1, 3: 1, 5: 1, 7: 1}
>>> frecuencias(4)
Traceback (most recent call last):
    File "<pyshell\#0>", line 1, in <module>
        frecuencias(4)
    File "frecuencias.py", line 12, in frecuencias
        for v in seq:
    TypeError: 'int' object is not iterable

Encapsulación

La encapsulación nos dice que los atributos y algunos métodos de nuestros objetos tienen la propiedad de ser privados o públicos, es decir nos permite limitar el acceso a nuestras variables y métodos, desde el código, pero fuera de nuestra propia clase, es decir que si tratamos de acceder a algún atributo o método que hayamos declarado como privado desde fuera de nuestra clase, sin que sea desde un objeto perteneciente a esta, no lo podremos hacer.

En Python no existen los modificadores de acceso (private, public) notables, los modificadores vienen establecidos por el nombre del método o atributo. Para iniciar un método o atributo como privado, el nombre de dicho método o atributo debe sólo comenzar con dos guiones bajo ("__"), de lo contrario lo toma como un método público. 
 
class Prueba(objetc):
       def __init__(self):
                self.__privado = "Soy Privado"
                self.privado = "Soy Publico"
       def __metodoprivado(self):
                print "Soy Privado"
       def metodopublico(self):
                print "Soy Publico"
objeto = Prueba()
print objeto.privado
print objeto.__privado
 
El resultado:
Soy Publico
Traceback (most recent call last):
   File "encapsulacion.py", line 17, in 
        print objeto.__privado
AtributeError: 'Prueba' object has no attribute '__privado' 
 
Como vemos Python nos muestra un error como si el atributo __privado no existiera ya que al ponerle los dos guines bajos adelante lo estamos haciendo privado.

A continuación haremos la prueba con los métodos:

class Prueba(objetc):
        def __init__(self):
                self.__privado = "Soy Privado"
                self.privado = "Soy Publico"
        def __metodoprivado(self):
                print "Soy Privado"
        def metodopublico(self):
                print "Soy Publico"
objeto = Prueba()
objeto.metodopublico()
objeto.__metodoprivado()

El resultado:

Soy Publico
Traceback (most recent call last):
   File "encapsulacion.py", line 17, in 
        objeto.__metodoprivado
AtributeError: 'Prueba' object has no attribute '__metodoprivado'
 
Nuevamente nos muestra el error de antes.

Cómo acceder a un atributo privado?

Esto se hace a través de los famosos "getters" y "setters", los cuales son métodos específicos que nos permiten obtener dichos valores.
 
class Prueba(objetc):
        def __init__(self):
                self.__privado = "Soy Privado"
                self.privado = "Soy Publico"
        def __metodoprivado(self):
                print "Soy Privado"
        def metodopublico(self):
                print "Soy Publico"
        def getPrivado(self):
                return self.__privado
objeto = Prueba()
print objeto.getPrivado()

El resultado:

Soy Privado

Como vemos nos imprime el valor del atributo privado __privado a través de un método get que sería como un método auxiliar.

Y si queremos modificar el valor del atributo privado, lo hacemos a través de un método set:

class Prueba(objetc):
        def __init__(self):
                self.__privado = "Soy Privado"
                self.privado = "Soy Publico"
        def __metodoprivado(self):
                print "Soy Privado"
        def metodopublico(self):
                print "Soy Publico"
        def getPrivado(self):
                return self.__privado
        def setPrivado(self,valor):
                self.__privado = valor
objeto = Prueba()
objeto.setPrivado("Hola Codigo")
print objeto.getPrivado()

El resultado:
Hola Codigo

Y, Cómo acceder a un método privado?

Esto solamente se puede hacer desde nuestra propia clase, a través de otro método:

class Prueba(objetc):
        def __init__(self):
                self.__privado = "Soy Privado"
                self.privado = "Soy Publico"
        def __metodoprivado(self):
                return "Soy Privado"
        def metodopublico(self):
                print "Soy Publico"
        def getPrivado(self):
                return self.__privado
        def setPrivado(self):
                self.__privado = self.__metodoprivado()
objeto = Prueba()
objeto.setPrivado()
print objeto.getPrivado()

El resultado: 

Soy Privado
 
También tenemos otros conceptos, como polimorfismo o sobrecarga de métodos, esta última podemos crearla de cierta manera, ya que en Python no existe sobrecarga de métodos, pero podemos crearla a través de las variables, es decir tener distintas variables y utilizarlas de acuerdo al valor recibido; pero sobrecarga de métodos como tal no existe, así tampoco el polimorfismo, ya que es un lenguaje de tipado dinámico.   

Métodos especiales

Así como el constructor, __init__, existen diversos métodos especiales que, si están definidos en nuestra clase, Python los llamará por nosotros cuando se utilice una instancia en situaciones particulares.

Un método para mostrar objetos

Para mostrar objetos, Python indica que hay que agregarle a la clase un método especial, llamado __str__ que debe devolver una cadena de caracteres con lo que queremos mostrar. Ese método se invoca cada vez que se llama a la función str.

El método __str__ tiene un solo parámetro, self.

En nuestro caso decidimos mostrar el punto como un par ordenado, por lo que escribimos el siguiente método dentro de la clase Punto:

def __str__(self):
    """ Muestra el punto como un par ordenado. """
    return "(" + str(self.x) + ", " + str(self.y) + ")"
 
Una vez definido este método, nuestro punto se mostrará como un par ordenado cuando se necesite una representación de cadenas.

>>> p = Punto(-6,18)
>>> str(p)
'(-6, 18)'
>>> print p
(-6, 18)
 
Vemos que con str(p) se obtiene la cadena construida dentro de __str__, y que internamente Python llama a __str__ cuando se le pide que imprima una variable de la clase Punto.

Nota Muchas de las funciones provistas por Python, que ya hemos utilizado en unidades anteriores, como str, len o help, invocan internamente a los métodos especiales de los objetos.
Es decir que la función str internamente invoca al método __str__ del objeto que recibe como parámetro. Y de la misma manera len invoca internamente al método __len__, si es que está definido.
Cuando mediante dir vemos que un objeto tiene alguno de estos métodos especiales, utilizamos la función de Python correspondiente a ese método especial.

Métodos para operar matemáticamente

Ya hemos visto un método que permitía restar dos puntos. Si bien esta implementación es perfectamente válida, no es posible usar esa función para realizar una resta con el operador -.

>>> p = Punto(3,4)
>>> q = Punto(2,5)
>>> print p - q
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for -: 'Punto' and 'Punto'
 
Si queremos que este operador (o el equivalente para la suma) funcione, será necesario implementar algunos métodos especiales.

def __add__(self, otro):
    """ Devuelve la suma de ambos puntos. """
    return Punto(self.x + otro.x, self.y + otro.y)
 
def __sub__(self, otro):
    """ Devuelve la resta de ambos puntos. """
    return Punto(self.x - otro.x, self.y - otro.y)
 
El método __add__ es el que se utiliza para el operador +, el primer parámetro es el primer operando de la suma, y el segundo parámetro el segundo operando. Debe devolver una nueva instancia, nunca modificar la clase actual. De la misma forma, el método __sub__ es el utilizado por el operador -.
Ahora es posible operar con los puntos directamente mediante los operadores, en lugar de llamar a métodos:

>>> p = Punto(3,4)
>>> q = Punto(2,5)
>>> print p - q
(1, -1)
>>> print p + q
(5, 9)
 
De la misma forma, si se quiere poder utilizar cualquier otro operador matemático, será necesario definir el método apropiado.

Nota La posibilidad de definir cuál será el comportamiento de los operadores básicos (como +, -, *, /), se llama sobrecarga de operadores.

No todos los lenguajes lo permiten, y si bien es cómodo y permite que el código sea más elegante, no es algo esencial a la Programación Orientada a Objetos.

Entre los lenguajes más conocidos que no soportan sobrecarga de operadores están C, Java, Pascal, Objective C. Entre los lenguajes más conocidos que sí soportan sobrecarga de operadores están Python, C++, C#, Perl, Ruby.
SHARE

Milan Tomic

Hi. I’m Designer of Blog Magic. I’m CEO/Founder of ThemeXpose. I’m Creative Art Director, Web Designer, UI/UX Designer, Interaction Designer, Industrial Designer, Web Developer, Business Enthusiast, StartUp Enthusiast, Speaker, Writer and Photographer. Inspired to make things looks better.

  • Image
  • Image
  • Image
  • Image
  • Image
    Blogger Comment
    Facebook Comment

0 comentarios:

Publicar un comentario