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.
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 clasePunto
, 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
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:
El resultado:
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) + ")"
>>> 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
Es decir que la función
Cuando mediante
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'
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)
__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)
Nota La posibilidad de definir cuál será el comportamiento de los operadores básicos (como
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.
+
, -
, *
, /
), 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.
0 comentarios:
Publicar un comentario