Programación orientada a objetos en Python
Introducción a la programación orientada a objetos
Este artÃculo es una introducción a la programación orientada a objetos (POO, o OOP por sus siglas en inglés), uno de los paradigmas de programación estructurada más importante hoy en dÃa.
El artÃculo usa el lenguaje Python para los distintos ejemplos, y explica la implementación de la POO en ese lenguaje, aunque aborda conceptos generales que son fácilmente trasladables a otros lenguajes.
Los fragmentos de código se señalan usando un estilo de texto diferente (ancho fijo) y, cuando se utiliza el intérprete de python, se prefija cada lÃnea con los sÃmbolos ‘>>>’.
Esta introducción presupone conocimientos básicos de programación y del lenguaje python.
Formas de pensar: paradigmas de programación
Uno de los elementos básicos a la hora de realizar un programa es la modelización del problema que pretende resolver. Es preciso localizar las variables y los aspectos relevantes, asà como comprender los pasos necesarios para obtener el resultado a partir de los datos iniciales, etc. Es decir, abstraer el problema, reduciendo sus detalles de forma que podamos trabajar con pocos elementos cada vez.
La algoritmia plantea la forma óptima de resolver problemas concretos, tales como la ordenación de una lista de elementos, la búsqueda de un elemento en un conjunto, la manipulación de conjuntos de datos, etc. Sin embargo, no proporcionan un marco general, un enfoque, que nos permita plantear y formular las soluciones a un problema de forma coherente.
Los paradigmas de programación llenan ese hueco, proporcionando guÃas tanto sobre cómo realizar la abstracción de los datos como sobre el control de la ejecución. Es decir, los paradigmas de programación son herramientas conceptuales para analizar, representar y abordar los problemas, presentando sistematizaciones alternativas o complementarias para pasar del espacio de los problemas al de las implementaciones de una solución.
Es muy recomendable y productivo comprender el enfoque de distintos paradigmas, puesto que presentan estrategias alternativas, incrementando nuestras herramientas disponibles y haciéndono reflexionar sobre muchas de las tareas que realizamos al crear un programa. Además, a menudo ocurre que unos problemas se formulan de forma muy clara si se los analiza según una perspectiva determinada, mientras que producen una gran complejidad vistos de otra manera.
Algunos paradigmas habituales
Algunas de las formas de pensar los problemas que ha llegado a sistematizarse como paradigmas de programación son:
- La programación modular: Los programas se forman por partes separadas llamadas módulos. Estos funcionan de forma independiente entre sÃ, o se relacionan a través de interfaces bien definidas.
- La programación procedural: Un programa se compone de procedimientos (subrutinas, métodos o funciones) que son fragmentos de código que pueden llamarse desde cualquier punto de la ejecución de un programa, incluÃdos otros procedimientos o él mismo. (ej. ALGOL)
- La programación estructurada: Se puede expresar cualquier programa utilizando únicamente tres estructuras de control: secuencial, condicional e iterativa. Los programas se componen de partes menores con un único punto de entrada, y se trata de aislarlos para evitar la complejidad que introducen los efectos colaterales. (ej. Pascal, C, Ada)
- La programación imperativa: Un programa se puede definir en términos de estado, y de instrucciones secuenciales que modifican dicho estado. (ej. C, BASIC)
- La programación declarativa: Es posible expresar un programa a través de condiciones, proposiciones o restricciones, a partir de los que se obtiene la solución mediante reglas internas de control. (ej. PROLOG)
- La programación funcional: Se puede expresar un programa como una secuencia de aplicación de funciones. Elude el concepto de estado del cómputo y no precisa de las estructuras de control de la programación estructurada. (ej. LISP, Haskell)
- La programación orientada a objetos: Los programas se definen en términos de «clases de objetos» que se comunican entre sà mediante el envÃo de mensajes. Es una evolución de los paradigmas de la programación procedural, estructurada y modular, y se implementa en lenguajes como Java, Smalltalk, Python o C++.
Programación multiparadigma
Los paradigmas de programación son idealizaciones, y, como tales, no siempre se presentan de forma totalmente ‘pura’, ni siempre resultan incompatibles entre sÃ. Cuando se entremezclan diversos paradigmas se produce lo que se conoce como programación multiparadigma.
El uso de distintos modelos, según se adapten mejor a las diversas partes de un problema o a nuestra forma de pensamiento, resulta también más natural y permite expresar de forma más clara y concisa nuestras ideas.
Algunos lenguajes, como Haskell o Prolog están diseñados para encajar perfectamente en la visión de un paradigma particular, mientras que otros, como python, se adaptan especialmente bien a la programación multiparadigma y dan gran libertad a la hora de resolver un problema, al no imponen un mismo patrón para todos los casos.
Tipos de datos
Una forma de almacenar y representar una fecha en un programa podrÃa ser la siguiente:
d = 14 m = "Noviembre" a = 2006 def dime_fecha(dia, mes, anho): return "%i de %s de %i del calendario gregoriano" % (dia, mes, anho) print dime_fecha(d, m, a)
para obtener la siguiente salida:
"14 de Noviembre de 2006"
En este ejemplo, para representar y manipular lo que conceptualmente entendemos como una fecha, se utilizan las variables d
, m
y a
como almacenes de datos, y un procedimiento o función, de nombre dime_fecha
, para realizar la representación de la fecha almacenada.
Las variables permiten almacenar tipos de datos concretos como los enteros (d
y a
) o las cadenas de texto (m
), y la función realiza su trabajo devolviendo otro tipo concreto, una cadena de texto.
Uno de los problemas de este enfoque es que no refleja demasiado bien el concepto fecha que usamos como modelo mental al implementarlo como un conjunto de tres variables no relacionadas entre sà (d
, m
y a
). Es decir, las tres variables tienen una unidad conceptual en nuestra mente, que no se refleja en la forma de expresar el problema. De la misma manera, la función dime_fecha
desconoce igualmente la relación entre sus parámetros, que podrÃa ser totalmente arbitraria.
Esta situación, que puede parecer una cuestión de ‘estilo’, implica un riesgo de problemas de sincronización entre variables ‘relacionadas’, dispersión en distintas partes del código de manipulaciones que son relevantes al conjunto, pérdida del valor semántico de cada variable que puede dar lugar a errores al transformar el código…
Un ejemplo de desconexión semántica entre la implementación y el concepto es que d
puede ser cualquier valor entero, mientras que un dÃa tiene un valor situado entre 1 y 31, m
es una cadena de texto dentro de un número limitado de opciones, o a
no puede tomar valores negativos. Por supuesto que esto se podrÃa solucionar con código que hiciese comprobaciones adicionales, pero en este caso queremos resaltar cómo un programa es un modelo aproximado a un problema y cómo serÃa preferible que la implementación pudiese representar de forma más ajustada el comportamiento del modelo elegido.
Una posible solución serÃa disponer de un tipo de datos que represente de forma más adecuada el concepto de fecha y que nos permita manipular fechas de la misma manera que manipulamos números enteros, números reales o cadenas de texto, independientemente de la implementación interna que los sustente.
Clases y objetos
Al enunciar algunos tipos de paradigmas hemos visto que la programación orientada a objetos define los programas en términos de «clases de objetos» que se comunican entre sà mediante el envÃo de mensajes. En este apartado aclararemos qué se entiende en ese contexto por clases y objetos.
Las clases surgen de la generalización de los tipos de datos y permiten una representación más directa de los conceptos necesarios para la modelización de un problema permitiendo definir nuevos tipos al usuario.
Las clases permiten agrupar en un nuevo tipo los datos y las funcionalidades asociadas a dichos datos, favoreciendo la separación entre los detalles de la implementación de las propiedades esenciales para su uso. A esta cualidad, de no mostrar más que la información relevante, ocultando el estado y los métodos internos de la clase, es conocida como «encapsulación», y es un principio heredado de la programación modular.
Un aspecto importante en el uso de clases es que no se manipulan directamente (salvo en lo que se conoce como metaprogramación), sino que sirven para la definición nuevos tipos. Una clase define propiedades y comportamiento que se muestran en los entes llamados objetos (o instancias de una clase). La clase actúa como molde de un conjunto de objetos, de los que se dice que pertenecen a la clase.
Trasladando estos conceptos a los números enteros (tipo int), se puede decir que las variables que almacenan números enteros son objetos o instancias de la clase int o que pertenecen a la clase int.
En términos más abstractos, podemos pensar en las clases como definiciones y en los objetos como expresiones concretas de dichas definiciones.
FisonomÃa de una clase
En python, el esquema para la definición de una clase es el siguiente:
class NombreClase: <instrucción_1> ... <instrucción_n>
en donde class es una palabra reservada que indica la declaración de una clase, NombreClase una etiqueta que da nombre a la clase y, los dos puntos, que señalan el inicio del bloque de instrucciones de la clase.
El cuerpo de instrucciones de la clase puede contener tanto asignaciones de datos como definiciones de funciones. Este bloque de instrucciones se encuentra en el nuevo espacio de nombres con el nombre de la clase, que se genera, a su vez, con la creación de cada objeto. En ambos casos, en el espacio de nombres de la clase y del objeto, es posible acceder a los elementos que lo integran usando el operador punto, como en el caso de los espacios de nombres de un módulo (e.j. math.pi, objeto.dato, objeto.funcion()).
Si la primera instrucción del cuerpo de la clase es una cadena de texto, ésta se usa como cadena de documentación de la clase.
Nuestro ejemplo podrÃa reescribirse en términos de una clase asÃ:
class Fecha: "Ejemplo de clase para representar fechas" dia = 14 mes = "Noviembre" anho = 2006 def dime_fecha(self): return "%i de %s de %i" % (Fecha.dia, Fecha.mes, Fecha.anho) mi_fecha = Fecha() print mi_fecha.dia, mi_fecha.mes, mi_fecha.anho print mi_fecha.dime_fecha()
En el fragmento de código anterior definimos la clase Fecha
y ella asignamos valores a las etiquetas dia
, mes
y anho
(atributos de la clase), y definimos una función (un método de la clase), dime_fecha
.
Fuera de la definición de la clase creamos (instanciamos) un objeto perteneciente a la clase Fecha
. Esta instanciación se realiza utilizando la notación de llamada a función, y podemos entenderla como una llamada a una función que devuelve un objeto de la clase Fecha. El objeto obtenido es asignado a la etiqueta mi_fecha
.
En la siguientes instrucciones mostramos por pantalla los valores de las propiedades dia
, mes
y anho
y el resultado de la llamada al método dime_fecha
, usando el operador punto para acceder a las propiedades y métodos del objeto mi_fecha
, ya que, como hemos mencionado antes, forman parte del espacio de nombres del objeto.
La llamada al método dime_fecha
no incluye ningún parámetro, pero podemos comprobar que, de forma contradictoria, la declaración del método indica uno, self
. Este parámetro es añadido automáticamente por el intérprete en las llamadas a los métodos de una clase, y contiene una referencia al objeto que recibe la señal (el que recibe una llamada a un método). self
permite acceder a las propiedades y métodos de un objeto concreto y aclararemos su significado y uso más adelante. Por ahora nos basta saber que es necesario incluir self
como primer parámetro de los métodos definidos en una clase y que no es preciso incluir el parámetro implÃcito self
al realizar llamadas a un método a través del objeto al que pertenece.
En realidad, en el ejemplo anterior, la llamada mi_fecha.dime_fecha() es una forma más cómoda de escribir (azúcar sintáctico) Fecha.dime_fecha(mi_fecha), lo que explica la presencia del parámetro.
Un paseo entre objetos
Hasta el momento hemos visto cómo las clases definen los datos (o estado) y comportamiento de los objetos a través de atributos (o propiedades) y métodos.
Ahora vamos a hacer un recorrido, utilizando una sesión interactiva del intérprete y las capacidades de introspección de python, para explicar el uso y el comportamiento de los objetos.
Veremos algunas caracterÃsticas de los objetos:
- Identidad. Los objetos se diferencian entre sÃ, de forma que dos objetos, creados a partir de la misma clase y con los mismos parámetros de inicialización, son entes distintos.
- Definen su comportamiento (y operar sobre sus datos) a través de métodos, equivalentes a funciones.
- Definen o reflejan su estado (datos) a través de propiedades o atributos, que pueden ser tipos concretos u otros objetos.
Pasamos a ver una sesión interactiva del intérprete:
>>> mi_fecha = Fecha()
>>> mi_fecha_2 = Fecha()
>>> mi_fecha is mi_fecha_2
False
>>> print mi_fecha
<__main __.Fecha instance at 0x00B184B8>
>>> print mi_fecha_2
<__main __.Fecha instance at 0x00B18328>
Vemos que tanto mi_fecha
como mi_fecha_2
son instancias de la clase Fecha
(en el módulo __main__
) y cada una tiene su propio espacio de memoria independiente.
>>> print mi_fecha.dime_fecha()
14 de Noviembre de 2006
>>> print mi_fecha.dia
14
>>> print mi_fecha_2.dia
14
>>> Fecha.dia = 16
>>> print Fecha.dia
16
>>> print mi_fecha.dia
16
>>> print mi_fecha_2.dia
16
>>> mi_fecha.dia = 30
>>> print Fecha.dia
16
>>> print mi_fecha.dia
30
>>> print mi_fecha_2.dia
16
Este último experimento observamos cómo, pese a que mi_fecha
y mi_fecha_2
son dos instancias distintas de la clase Fecha
, ambas hacen referencia a través de su atributo dia al mismo valor de la clase Fecha.dia
. Sin embargo, cuando se produce una asignación a ese atributo (a esa etiqueta, en realidad) en uno de los objetos vemos cómo, a partir de entonces, ese objeto dispone de un valor ‘individual’ asociado al atributo.
Ese comportamiento revela la existencia de atributos (o estado) compartidos por todas las instancias de una clase y de atributos (o estado) ligados a una instancia particular. Los primeros se sitúan en el espacio de nombres de la clase, y vemos cómo se pueden hacer asignaciones en la sesión interactiva anterior, o lecturas, en el método dime_fecha
del ejemplo anterior. Los segundos se sitúan en el espacio de nombres de cada objeto, y ahà es donde resulta útil el parámetro self
.
Antes de proseguir, podemos escudriñar qué sÃmbolos son accesibles y qué contienen los espacios de nombres de la clase Fecha y de los objetos mi_fecha
y mi_fecha_2
. Usamos la función dir para lo primero, y el atributo __dict__
para lo segundo, ya que en python los espacios de nombres se implementan como diccionarios:
>>> dir(Fecha)
['__doc__', '__module__', 'anho', 'dia', 'dime_fecha', 'mes']
>>> dir(fecha3)
['__doc__', '__module__', 'anho', 'dia', 'dime_fecha', 'mes']
>>> dir(fecha4)
['__doc__', '__module__', 'anho', 'dia', 'dime_fecha', 'mes']
En los tres podemos acceder a los mismos sÃmbolos. Sin embargo…
>>> Fecha.__dict__
{'__module__': '__main__', 'anho': 2006, 'dime_fecha':
>>> mi_fecha_2.__dict__
{}
>>> mi_fecha.__dict__
{'dia': 30}
El espacio de nombres de la clase es el que alberga los nombres de los atributos y métodos indicados en la declaración de la clase, además de los atributos especiales __doc__
, que contiene la cadena de documentación y __module__
, que contiene el nombre del módulo al que pertenece la clase. Esos sÃmbolos son accesibles también para los objetos pertenecientes a la clase (tal como nos indicaba dir()).
Podemos ver también que el espacio de nombres del objeto mi_fecha_2
se encuentra vacÃo, mientras que el del objeto mi_fecha
contiene el atributo dÃa
. Esto se debe a que anteriormente realizamos una asignación al atributo dia
del objeto mi_fecha
(dándole el valor 30). El espacio de nombres recoge esa versión particular del atributo, que no aparece en el otro objeto, puesto que accede al atributo de clase.
A los atributos compartidos por todos los objetos de una clase se los denomina atributos de clase, cuando se desea diferenciarlos de los atributos que pertenecen a cada instancia en particular.
Acceso individualizado a objetos: self
Ahora que hemos visto cómo usar atributos de clase para compartir datos entre todos los objetos de una misma clase, también nos interesa conocer cómo utilizar atributos comunes a una clase pero que puedan tomar valores distintos para cada uno de los objetos.
Retomamos ahora el misterioso parámetro self
que vimos estaba presente como primer parámetro de todos los métodos de una clase. En su momento ya comentamos que self
contiene una referencia al objeto que recibe la señal (el objeto cuyo método es llamado), por lo que podemos usar esa referencia para acceder al espacio de nombres del objeto, es decir, a sus atributos individuales.
>>> class F: ... i = 5 ... def dime_i(self): ... return F.i ... def dime_mi_i(self): ... return self.i ... >>> a = F() >>> a.dime_i() 5 >>> a.dime_mi_i() 5 >>> a.i = 6 >>> a.dime_i() 5 >>> a.dime_mi_i() 6 >>> a.i 6
En este ejemplo el método dime_i
usa el atributo de clase y retorna su valor, 5, mientras que el método dime_mi_i
usa una referencia al objeto y devuelve el valor 6, el asignado al atributo del objeto a
. Asà accedemos selectivamente al atributo de clase i
o al atributo de instancia i
.
Algo que puede aclarar algo más la naturaleza del parámetro self
, es recordar que, cuando hacemos una llamada a un método un_metodo
de un objeto un_objeto
perteneciente a la clase UnaClase
, el intérprete traduce la expresión un_objeto.un_metodo()
como UnaClase.un_metodo(un_objeto)
.
El uso de self
responde a la forma particular en que python implementa el soporte de objetos, aunque lenguajes como Java o C++ utilizan mecanismos similares con la palabra reservada this
. En python self
no es una palabra reservada y cualquier etiqueta usada como primer parámetro valdrÃa igualmente, aunque se desaconseja totalmente el uso de otras etiquetas, por tratarse de una convención muy arraigada en el lenguaje que hace el código más legible, facilita el resaltado de sintaxis, etc.
El método de inicialización __init__
Hemos visto ya cómo definir y usar atributos de clase y de instancia. Pero en python no es necesaria la declaración de variables, por lo que cualquier atributo al que se realice una asignación en el código se convierte en un atributo de instancia:
>>> a = F()
>>> a.i = 6
>>> a.atr = 3
>>> print a.atr
3
>>> a.__dict__
{'i': 6, 'atr': 3}
Una funcionalidad muy conveniente que hemos visto en la declaración de atributos de clase es la de realizar su inicialización en el cuerpo de la clase. Para poder inicializar los atributos de una instancia existe un método especial que permite actuar sobre la inicialización del objeto. Dicho método se denomina __init__
(con dos guiones bajos al principio y al final) y admite cualquier número de parámetros, siendo el primero la referencia al objeto que es incializado (self
, por convención), que nos permite realizar las asignaciones a atributos de la instancia. Este método se ejecuta siempre que se crea un nuevo objeto de la clase, tras la asignación de memoria y con los atributos de clase inicializados, y se corresponde parcialmente con el concepto de constructor de la clase existente en otros lenguajes.
Con la referencia que nos proporciona self
podemos inicializar atributos de instancia, con valores predeterminados si lo deseamos, como en cualquier definición de función:
class Clase: def __init__(self, x=2): self.x = x self.a = x**2 self.b = x**3 self.c = 999 def dime_datos(self): return "Con x=%i obtenemos: a=%i, b=%i, c=%i" % (self.x, self.a, self.b, self.c) a1 = Clase(2) a1.dime_datos() a2 = Clase(3) a2.dime_datos()
Salida:
Con x=2 obtenemos: a=4, b=8, c=999
Con x=3 obtenemos: a=9, b=27, c=999
Propiedades y Atributos
Aunque en la terminologÃa general de la programación orientada a objetos atributo y propiedad se pueden utilizar como sinómimos, en python se particulariza el uso de propiedades para un tipo especial de atributos cuyo acceso se produce a través de llamadas a funciones.
class ClaseC(object): def __init__(self): self.__b = 0 def __get_b(self): return self.__b def __set_b(self, valor): if valor > 10: self.__b = 0 else: self.__b = valor b = property(__get_b, __set_b, 'Propiedad b') c1 = ClaseC() print c1.b # b es 0 c1.b = 5 print c1.b # b es 5 c2 = ClaseC() print c2.b # b es 0 c2.b = 12 print c2.b #b es 0
En este ejemplo se define la propiedad b
, cuyo comportamiento es: cuando se realiza una asignación, toma el valor entregado si es menor que 10, o 0 en caso contrario, y lo almacena en un atributo privado __b
. Cuando se produce la lectura de b
, devuelve el valor guardado en el atributo privado.
En Python, para poder usar propiedades en una clase es necesario hacerla derivar de la clase object
de ahà que aparezca al lado del nombre de la clase ClaseC
. En un apartado posterior se explicará en qué consiste la derivación de clases.
La signatura de la función property
, que define una propiedad de la clase es la siguiente:
nombre_propiedad = property(get_f, set_f, del_f, doc)
donde get_f
es la función llamada cuando se produce la lectura del atributo; set_f
la función llamada cuando se produce una asignación al atributo; del_f
la función llamada cuando se elimina el atributo, y doc
es una cadena de documentación de la propiedad.
Las propiedades resultan muy útiles para realizar la validación de los valores asignados, la transformación o cálculo de valores devueltos y, además, dotan de gran flexibilidad a la hora de escribir el código, puesto que es posible empezar usando atributos y luego sustituirlo por una propiedad que realice funciones adicionales, a medida que el código lo requiera y simplemente cambiando el código de la clase, sin afectar al código cliente de la clase.
Herencia y derivación de clases
Vemos que el uso de clases hace más adecuada la representación de conceptos en nuestros programas. Además, es posible generar una clase nueva a partir de otra, de la que recibe su comportamiento y estado (métodos y atributos), adaptándolos o ampliándolos según sea necesario.
De una clase que representase el concepto de vehÃculo podrÃamos derivar otras para representar automóviles, bicicletas, barcos o aviones, donde cada uno de ellos mantiene atributos y métodos comunes (peso, color, velocidad máxima, número de pasajeros), mientras que otros son exclusivos (número de ruedas, número de hélices, número de reactores, altitud máxima de vuelo…).
En términos matemáticos podemos expresarlo diciendo que es posible establecer relaciones de pertenencia entre clases. Si tenemos una clase A y de ella derivamos una clase B, podemos decir que «B es una A«, o que «B es una especialización de A«.
En la terminologÃa de la POO se dice que «B hereda de A«, «B es una clase derivada de A«, «A es la clase base de B«, «A es superclase de B» o «A es clase madre de B«.
Esto facilita la reutilización del código, puesto que se pueden implementar los comportamientos y datos básicos en una clase base y especializarlos en las clases derivadas.
En un programa hipotético que trabajase con formas geométricas, podrÃamos tener una clase base Forma y clases derivadas de ella como Triangulo, Cuadrado, Circulo… En la clase base podrÃamos definir un método dibuja que representa la figura en pantalla, y un método area y otro perÃmetro que calculan el área y el perÃmetro, respectivamente.
En el lenguaje python, para expresar que una clase deriva, desciende o es heredera de otra u otras clases se añade tras el nombre, en la declaración de la clase, una tupla con los nombres de las clases base.
El siguiente ejemplo crea una ClaseA
y de ella deriva una ClaseB
que inicializa un atributo adicional, cambia el comportamiento de un método y hereda los atributos de la clase madre:
class ClaseA: def __init__(self, x): self.a = x self.b = 2 * x def muestra(self): print "a=%i, b=%i" % (self.a, self.b) class ClaseB(ClaseA): def __init__(self, x, y): ClaseA.__init__(self, x) self.c = 3 * (x + y) + self.b def muestra(self): print "a=%i, b=%i, c=%i" % (self.a, self.b, self.c) print "Objeto A" a = ClaseA(2) print a.__dict__ a.muestra() print "ObjetoB" b = ClaseB(2, 3) print b.__dict__ b.muestra()
Este código al ejecutarse devuelve:
Objeto A
{'a': 2, 'b': 4}
a=2, b=4
ObjetoB
{'a': 2, 'c': 19, 'b': 4}
a=2, b=4, c=19
La clase ClaseA
muestra funcionalidades que ya hemos ido viendo en esta introducción (atributos de instancia a
y b
, método muestra
). La clase ClaseB
se declara como descendiente de la ClaseA
, por lo que hereda sus métodos y atributos y modifica algo el comportamiento de la clase madre. Por una parte, en su método de inicialización llama al método de la clase madre con los parámetros deseados, y, posteriomente, añade un nuevo atributo de instancia que no existe en la clase madre. Por otro lado, redefine un método existente en ClaseA
, cambiando la implementación.
Esta última técnica, en la que un mismo nombre se asocia a comportamientos distintos se conoce como polimorfismo, y se habla de que el método se ha sobrecargado, es decir, se le asignan distintos significados en función de su contexto. El polimorfismo también alude a la posibilidad de utilizar, en un contexto en el que se espera una clase dada, cualquier clase derivada.
La clase object
Desde la versión 2.2 de python, se ha establecido una clase base llamada object
de la que se aconseja derivar todas las clases. Esto permite aprovechar una serie de caracterÃsticas sólo existentes en las «nuevas clases» (como la definición de propiedades), que se describen en la documentación de python.
En el ejemplo sobre el uso de propiedades se introdujo la herencia sin explicarla, derivando la clase de object
. Ahora deberÃa estar más clara la razón :).
JerarquÃas de clases
La herencia permite establecer relaciones entre clases, y, estas relaciones de pertenencia pueden ser a varios niveles, y ramificarse en lo que se denominan jerarquÃas de clases.
Un ejemplo de clásico para visualizar las jerarquÃas de clases es el que podrÃa servir para modelizar el conjunto de empleados de una empresa. En ella aparece una clase base Empleado
, que dispone de los atributos nombre
y departamento
; de ella descienden las subclases Gestor
(atributo adicional informes
) y Trabajador
(atributo adicional proyectos
), y de esta última derivan las clases Comercial
(atributo adicional participación
) e Ingeniero
(atributo original area
).
Gráficamente podrÃa representarse de la siguiente manera:
En el diagrama se puede ver en negrita los atributos que implementa cada clase, y en gris los que hereda de clases madre. El código (básico de inicialización) de las clases podrÃa ser el siguiente:
class Empleado: def __init__(self, nombre, departamento="general"): self.nombre = nombre self.departamento = departamento class Gestor(Empleado): def __init__(self, nombre): Empleado.__init__(self, nombre) self.informes = [] class Trabajador(Empleado): def __init__(self, nombre): Empleado.__init__(self, nombre) self.proyectos = [] class Comercial(Trabajador): def __init__(self, nombre, participacion=0.1): Trabajador.__init__(self, nombre) self.departamento = "ventas" self.participacion = participacion class Ingeniero(Trabajador): def __init__(self, nombre, area="mecánica"): Trabajador.__init__(self, nombre) self.departamento = "ingenierÃa" self.area = area
Naturalmente, cada una de las clases tendrÃa sus propios métodos que definirÃan comportamientos propios de cada clase o subclase, que no se han detallado en el ejemplo.
Una alternativa a la herencia es la composición, en el que una clase se compone de otras clases internas, y que puede ser una mejor alternativa en muchos casos.
Encapsulación y grados de privacidad
Uno de los principios que guÃan la POO, heredados de la programación modular, es el de encapsulación. Hemos visto cómo se puede agrupar comportamiento y datos gracias al uso de objetos, pero hasta el momento, tanto los atributos como los métodos que se definen en las clases correspondientes se convierten en métodos y atributos visibles para los usuarios de la clase (la API de la clase).
Se dice que un método o atributo es público si es accesible desde el exterior de la clase, mientras que se denomina privado en caso contrario.
Es deseable poder señalar qué métodos y atributos no deben utilizarse fuera de la clase para evitar exponer excesivamente los detalles de la implementación de una clase o un objeto.
Python utiliza para ello convenciones a la hora de nombrar métodos y atributos, de forma que se señale su carácter privado, para su exclusión del espacio de nombres, o para indicar una función especial, normalmente asociada a funcionalidades estándar del lenguaje.
_nombre
Los nombres que comienzan con un único guión bajo indican de forma débil un uso interno. Además, estos nombres no se incorporan en el espacio de nombres de un módulo al importarlo con «from … import *».
__nombre
Los nombres que empiezan por dos guiones bajos indican su uso privado en la clase.
Por ejemplo, en la definición de clase siguiente:
class ClaseA: def __init__(self, a) self.__a = a self.x = self.__a**2 + 2 self.y = self.__a**3 + 3
el atributo __a
es de uso interno de la clase y no se exporta directamente. A continuación podemos ver cómo trata los atributos ‘privados’ python:
>>> a = ClaseA(2)
>>> dir(a)
['_ClaseA__a', '__doc__', '__init__', '__module__', 'x', 'y']
>>> a.__dict__
{'x':6, 'y':11, '_ClaseA__a':2}
TodavÃa es posible acceder a __a
, pero el nombre es transformado a '_ClaseA__a'
, que lo señala como un atributo de uso privado.
__nombre__
Los nombres que empiezan y acaban con dos guiones bajos indican atributos «mágicos», de uso especial y que residen en espacios de nombres que puede manipular el usuario. Solamente deben usarse en la manera que se describe en la documentación de python y debe evitarse la creación de nuevos atributos de este tipo.
Algunos ejemplos de nombres «singulares» de este tipo son:
__init__
, método de inicialización de objetos
__del__
, método de destrucción de objetos
__doc__
, cadena de documentación de módulos, clases…
__class__
, nombre de la clase
__str__
, método que devuelve una descripción de la clase como cadena de texto
__repr__
, método que devuelve una representación de la clase como cadena de texto
__module__
, módulo al que pertenece la clase
Se puede consultar la lista de atributos de este tipo y su descripción detallada en la documentación de python.
Resúmen final
La programación orientada a objetos enuncia la posibilidad de escribir un programa como un conjunto de clases de objetos capaces de almacenar su estado, y que interactúan entre sà a través del envÃo de mensajes.
Las técnicas más importantes utilizadas en la programación orientada a objetos son:
- Abstracción: Los objetos pueden realizar tareas, interactuar con otros objetos, o modificar e informar sobre su estado sin necesidad de comunicar cómo se realizan dichas acciones.
- Encapsulación (u ocultación de la información): los objetos impiden la modificación de su estado interno o la llamada a métodos internos por parte de otros objetos, y solamente se relacionan a través de una interfaz clara que define cómo se relacionan con otros objetos.
- Polimorfismo: comportamientos distintos pueden estar asociados al mismo nombre
- Herencia: los objetos se relacionan con otros estableciendo jerarquÃas, y es posible que unos objetos hereden las propiedades y métodos de otros objetos, extendiendo su comportamiento y/o especializándolo. Los objetos se agrupan asà en clases que forman jerarquÃas.
Las clases definen el comportamiento y estado disponible que se concreta en los objetos.
Los objetos se caracterizan por:
- Tener identidad. Se diferencian entre sÃ.
- Definir su comportamiento a través de métodos.
- Definir o reflejar su estado a través de propiedades y atributos.
Gracias a la amable aportación de Óscar Carballal ahora es posible descargar una versión en PDF del artÃculo
Comments
Comment from Just D
Date: 12/06/2006, 1:11 pm
GrandÃsimo artÃculo! Felicitaciones.
Licenciás tus cosas bajo Creative Commons?
Saludos.
Comment from NKT
Date: 02/28/2007, 1:01 pm
Realmente muy interesante articulo. Me ha resuelto mucha de las dudas que tenia en cuanto a la jerarquia de clases en python (viniendo desde C++).
Comment from Jose Fabrega
Date: 08/16/2007, 10:32 pm
Me parece que este articulo es de gran ayuda, para todo el que se inicia en la programacion con python, por tal motivo lo recomiendo abiertamente.
Comment from henry
Date: 10/16/2007, 12:00 am
buen articulo, sirve para aprender mucho y sacarlo de problemas. aunque falto algo mas de relacion entre clases
Comment from Gonzalo Sanchez
Date: 02/12/2008, 3:41 pm
Me gusto mucho tu articulo sobre POO, buen material has puesto y con muy buena capacidad de sistensis, te he linkeado desde mi blog, espero que no te moleste.
El blog trata de programacion de video juegos usando python y pygame.
la direccion del blog es http://www.creatusjuegos.blogspot.com
Comment from pachi
Date: 02/12/2008, 6:13 pm
Gracias por vuestros comentarios, y me alegro de que os resulte útil.
Comment from lopz
Date: 03/14/2008, 8:39 pm
hola
Muy muy interesante, al fin entendà esto de las clases en python, me parecÃa un lio explicado de otra forma, sigue con el blog y a ver si te pones una de python+pygtk o threads
saludos!
Pingback from jhuss blog · Programación orientada a objetos en Python
Date: 03/24/2008, 7:20 pm
[…] si alguno de esos temas parece interesante, pues visite el siguiente articulo del blog de RVBurke, Programación orientada a objetos en Python […]
Comment from weoz
Date: 05/05/2008, 10:11 pm
Buenas me gustaria saber donde puedo encotrar mas info sobre este tema en español..
gracias es que aun no entiendo muy bien los conceptps
Comment from Eydolina
Date: 06/11/2008, 4:01 pm
Estuve leyendo un poco y me agrado la información es muy concreta..
gracias…..
:p
Comment from The Machine
Date: 08/15/2008, 7:58 am
WOW, excelente. Muy buen articulo, felicitaciones. Si pudieras colocar otro de GUI en Python serÃa fenomenal.
Comment from ingemar
Date: 09/02/2008, 10:51 pm
muy buen aporte para la comunidad, te felicito esta muy especifico y concreto tambien tiene buen uso en la parte tecnica.
sigue asi…..
Comment from cejota
Date: 04/13/2009, 5:59 am
Hola, muy buen articulo.
No encontre muchos en español que traten estos temas claramente.
Seria mucho pedir, si pudieras ampliar o hacer uno nuevo, sobre el uso de namespaces y modulos?
me enfrento al problema de tener directorios donde reparto las clases (modelos), las aplicaciones (scripts o clases donde tengo la logica) y config (datos especificos de la instalacion, entorno, y ubicacion en el SO de files), en config esta todos los datos especificos que pueden cambiar de maquina a maquina o usuario a usuario, mas un directorio de «datos», que serian los sources de lo que voy a procesar.
el caso es que necesitaria poder cargar esa config (digamos que es un xml) pero lo primero que llamo es a una aplicacion, que deberia poder levantar los datos que necesite del xml.
lo ideal seria tener un bootstrap, un punto comun de entrada donde cargue la config de entorno, y ahi vaya a la aplicacion, en donde ya sabra de donde cargar las clases y datos necesarios.
podrias orientarme como se hace esto en python? uso por trabajo un framework en php que esta armado asi y ahi basta con poner el string del entorno deseado para que busque ese file .Ejemplo si $user = claudio entonces config::env::$user.xml y busca en config/user/claudio.xml o import(‘config/env/’.$user.’.php’);
saludos
Comment from pachi
Date: 04/13/2009, 1:15 pm
Gracias por tu comentario cejota. Por ahora no tengo mucho tiempo para continuar esta serie de programación, pero tendré en cuenta tu sugerencia si algún dÃa la retomo.
En Python tienes mucha facilidad para hacer lo que preguntas, al ser bastante sencilla la organización de espacios de nombres y poder pasar de atributos a módulos o a clases sin necesitar cambios grandes en el código.
No comprendo bien tu problema, pero si se trata de cargar distintos entornos en función de una «clave» tienes muchas formas de hacerlo. Busca en la red por «dispatching».
Comment from airus
Date: 06/15/2009, 2:38 pm
Está muy buena la introducción.
Comment from Javier Albornoz
Date: 07/07/2009, 11:27 pm
¡Felicitaciones por el excelente trabajo!
Es muy didáctico y por esta razón le estoy recomendando a mis estudiantes que visiten su espacio para que se documenten en el tema.
Comment from adriana
Date: 07/15/2009, 1:16 am
excelente trabajo, felicidades el articulo es claro y preciso para lo que necesitaba
Comment from Pato
Date: 07/30/2009, 7:12 pm
Muy buen artÃculo. Estoy empezando con python y me ha dado una buena base que no encontraba en otros manuales. Saludos desde Quito-Ecuador.
Pingback from Introduccion a la programación orientada a objetos con python :Blog de Alex Vera
Date: 10/26/2009, 8:10 am
[…] visitar: Programación orientada a objetos en Python […]
Comment from wozgeass
Date: 11/08/2009, 3:58 am
buen articulo…
Comment from Daniel
Date: 11/28/2009, 11:02 am
wao… pues no dejo de sorprenderme con este lenguaje, viniendo de java o c++ uno queda como sin piso, es sencillo, facil, sin embargo la forma de hacer programas como uno tiene la idea en java por lo menos, queda efimera o dispersa en python y en mi caso me siento perdido …. :S gracias por el artÃculo serguiré leyendo.
ciao
Comment from pachi
Date: 11/28/2009, 12:15 pm
Daniel, algo que te puede hacer sentir más cómodo en python es pensar que algunas de las exigencias de Java o C++ pueden ser parte de los comentarios en lugar de código. Por ejemplo, en el caso de los atributos de una clase, que suele resultar «raro» no tener que declararlos en el constructor, se pueden documentar en la docstring de la clase y listo.
Comment from pizte
Date: 12/24/2009, 2:28 pm
Por fin un manual en español de clases! Debo reconocer que me resulta todavÃa bastante abstracto el tema de orientación a objetos, pero poco a poco le estoy cogiendo el truco, y este «miniHOWTO» ha sido un gran avance.
Felicitaciones por este buen trabajo. No te has enredado con explicaciones innecesarias que suelen venir en los manuales y lo has explicado de forma bastante gráfica.
Para los que estáis preguntando por la programación de GUI’s en Python, en la página de PyGTK http://pygtk.org/tutorial.html tienen un manual traducido al español, que aunque no está muy actualizado (data de 2005, para GTK+ 2.4, ahora estamos en la 2.16) pero sirve perfectamente para empezar a hacer cosas útiles, ya que los cambios entre versiones no han sido demasiado drásticos y todavÃa hay compatibilidad de funciones.
Un saludo!
P.D: Esta explicación de clases es lo que le falta al «superlibro» de la Universidad Jaume I http://marmota.act.uji.es/MTP/pdf/python.pdf para que sea el libro perfecto jeje
Comment from mateu
Date: 01/15/2010, 11:36 pm
muchas gracias ha sido una definicion muy clara y práctica.
Comment from m[cun]
Date: 09/13/2010, 9:07 pm
Muy interesante tu articulo lo agradezco.
Debo decirte que tienes un error en el primer ejemplo llamas a la función print fecha(d, m, a) y por supuesto da una excepción ya que no esta declarada.
la llamada correcta debe ser
print dime_fecha(d, m, a) solo por si algún despistado abandona la guÃa al no funcionarle el 1º articulo saludos y gracias nuevamente
Comment from pachi
Date: 09/13/2010, 10:46 pm
Muchas gracias m[cun] por tus comentarios. He corregido la errata que indicas.
Comment from Germán
Date: 12/16/2010, 7:19 pm
Pachi,
MuchÃsimas gracias por este artÃculo! 🙂
Estoy comenzando a programar en Python, vengo del Fortran 77, y se me hacÃa una ensalada de definiciones en la cabeza esto de POO. Gracias a tu artÃculo, ahora las cosas están mucho más claras!
Un fuerte abrazo,
Germán.
Comment from Heber
Date: 01/06/2012, 2:23 am
Excelente, estoy apenas llegando a este lenguaje por pura afición y esto de las clases y los objetos no es fácil, pero luego de leer se me ha aclarado algo el panorama. Gracias.
Comment from gabo
Date: 02/09/2012, 8:44 am
Excelente, Gracias
el planeta es mejor con tutoriales como estos.
Comment from Albeiro
Date: 12/12/2012, 7:29 pm
Me ha resultado muy útil el tutorial, me ha permitido entender bien el asunto de clases y objetos
Pingback from Introducción a la programación orientada a objetos | Código Rockiano
Date: 01/24/2013, 10:34 pm
[…] bien menciona Rafael Villar Burke en su blog, la algoritmia consiste en la búsqueda de soluciones a un problema concreto, pero no nos […]
Comment from gabriel
Date: 11/26/2013, 3:33 am
che muy bueno, que simple esta, facil de entender la verdad muchas gracias, realmente me sirve para seguir avanzando ya que me habia trabado no logrando entender que es self. jaja al final era resimple. gracias de nuevo
Comment from Rafael Moyano
Date: 07/03/2014, 5:42 am
Rafael me parece que en el diagrama de clases, para la clase Comercial el valor del atributo departamento es ventas y no ingenerÃa.
Muy buen artÃculo, siempre vuelvo a tú blog a leerlo.
Comment from Moebius
Date: 09/25/2014, 6:50 am
Muy bien explicado, la verdad agradezco sinceramente tu aporte. He estado tratando de entender el «fondo» de ese concepto: La clase. Porque muchos sitios de internet simplemente se limitaban era a definir una clase y no explicaban ¿para qué usar una clase? ¿qué utilidad tiene usarla?.
No sé si entendà mal, pero lo que entendà es que en Python existen varios tipos de datos (int, float, complex, string, list, tuple etc), al yo crear una clase lo que estoy haciendo es crear un nuevo tipo de dato y la forma de manipular ese nuevo tipo de dato
Write a comment