Programación orientada a Objetos en Python
Contents
Programación orientada a Objetos en Python #
Introducción#
La Programación Orientada a Objetos (POO, en español; OOP, según sus siglas en inglés) es un paradigma de programación que parte del concepto de “objetos” como base, los cuales contienen información en forma de campos (a veces también referidos como atributos o propiedades) y código en forma de métodos.
Los objetos son capaces de interactuar y modificar los valores contenidos en sus campos o atributos (estado) a través de sus métodos (comportamiento).
Muchos de los objetos prediseñados de los lenguajes de programación actuales permiten la agrupación en bibliotecas o librerías, sin embargo, muchos de estos lenguajes permiten al usuario la creación de sus propias bibliotecas.
Algunas características clave de la programación orientada a objetos son herencia, cohesión, abstracción, polimorfismo, acoplamiento y encapsulamiento.
Su uso se popularizó a principios de la década de 1990. En la actualidad, existe una gran variedad de lenguajes de programación que soportan la orientación a objetos, estando la mayoría de éstos basados en el concepto de clases e instancias.
Tomado de Wikipedia
Desde el comienzo, Python fue diseñado como un leguaje orientado a objetos. Python es un lenguaje de programación orientado a objetos. Todo en Python es un objeto, con sus propiedades y métodos.
Breve historia#
Los conceptos de la POO tienen origen en Simula 67, un lenguaje diseñado para hacer simulaciones, creado por Ole-Johan Dahl y Kristen Nygaard, del Centro de Cómputo Noruego en Oslo. En este centro se trabajaba en simulaciones de naves, que fueron confundidas por la explosión combinatoria de cómo las diversas cualidades de diferentes naves podían afectar unas a las otras.
La idea surgió al agrupar los diversos tipos de naves en diversas clases de objetos, siendo responsable cada clase de objetos de definir sus “propios” datos y comportamientos.
La POO se fue convirtiendo en el estilo de programación dominante a mediados de los años 1980, en gran parte debido a la influencia de C++, una extensión del lenguaje de programación C. Su dominación fue consolidada gracias al auge de las interfaces gráficas de usuario, para las cuales la POO está particularmente bien adaptada. En este caso, se habla también de programación dirigida por eventos.
La terminología “objetos” y “orientada” en el sentido moderno de la programación orientada a objetos hizo su primer aparición en el MIT a finales del 1950s y principio de 1960s. Ya en 1960 en el entorno del grupo de inteligencia artificial, el término “objeto” era usado para referirse a elementos (LISP átomos) con propiedades (atributos).
Otro ejemplo temprano de programación orientada en el MIT fue Sketchpad creado por Ivan Sutherland en 1960–1961; en el glosario del informe técnico de 1963, Sutherland define la noción de “objeto” y de “instancia”.
Simula introdujo conceptos importantes que hoy en día son una parte esencial de la programación orientada a objetos, como clases, objetos, herencia y dynamic binding.
Mas recientemente ha surgido una serie de lenguajes que están principalmente orientados a objeto pero que también son compatibles con la programación procedural. Dos ejemplos de estos lenguajes son Python y Ruby.
Tomado de Wikipedia
Conceptos fundamentales#
La POO es una forma de programar que introduce nuevos conceptos, que superan y amplían conceptos antiguos ya conocidos en la programación. Entre ellos destacan los siguientes:
Clase#
Una clase
es una especie de “plantilla” en la que se definen los atributos y métodos predeterminados de un tipo de objeto. Esta plantilla se hace para poder crear objetos fácilmente. Al método de crear nuevos objetos mediante la lectura y recuperación de los atributos y métodos de una clase se le conoce como instanciación
.
Herencia#
Por ejemplo, herencia de la clase C a la clase D, es la facilidad mediante la cual la clase D hereda en ella cada uno de los atributos y operaciones de C, como si esos atributos y operaciones hubiesen sido definidos por la misma D.
Objeto#
Instancia de una clase. Entidad provista de un conjunto de propiedades o atributos (datos) y de comportamiento o funcionalidad (métodos), los mismos que consecuentemente reaccionan a eventos. Se corresponden con los objetos reales del mundo que nos rodea, o con objetos internos del sistema (del programa).
Método#
Algoritmo (función) asociado a un objeto (o a una clase de objetos), cuya ejecución se desencadena tras la recepción de un “mensaje”. Desde el punto de vista del comportamiento, es lo que el objeto puede hacer. Un método puede producir un cambio en las propiedades del objeto, o la generación de un “evento” con un nuevo mensaje para otro objeto del sistema.
Evento#
Es un suceso en el sistema (tal como una interacción del usuario con la máquina, o un mensaje enviado por un objeto). El sistema maneja el evento enviando el mensaje adecuado al objeto pertinente. También se puede definir como evento la reacción que puede desencadenar un objeto; es decir, la acción que genera.
Mensaje#
Una comunicación dirigida a un objeto, que le ordena que ejecute uno de sus métodos con ciertos parámetros asociados al evento que lo generó.
Propiedad o atributo#
Contenedor de un tipo de datos asociados a un objeto (o a una clase de objetos), que hace los datos visibles desde fuera del objeto y esto se define como sus características predeterminadas, y cuyo valor puede ser alterado por la ejecución de algún método.
Clases en Python#
Podemos se dijo arriba, imaginarnos una clase como una plantilla o un plano para construir objetos.
Para crear una clase en Python se usa el la palabra claveclass:
Supongamos, por ejemplo, que queremos crear una plataforma para recolectar toda la información personal que podamos de nuestros usuarios (nada parecido con la realidad) porque… sí.
Primero crearemos una clase que no haga nada.
Creando nuestra primera clase#
class Usuario:
pass
print(Usuario)
<class '__main__.Usuario'>
La razón de pass
es debido a que una clase necesita al menos una línea para poder existir.
Observe que las clases tienen definida una salida por defecto para la función print. el término main indica el espacio de trabajo actual (workspace). Por lo general será main al definir una clase, pero no necesariamente es así.
Convenio de notación#
Un convenio de notación es que, para crear los nombres de las clases se comience con mayúsculas.
Creando nuestros primeros objetos#
Ok. Ahora creemos un objeto de tipo Usuario:
u1 = Usuario()
u2 = Usuario()
u1
<__main__.Usuario at 0x7fc2a0c3dc50>
Como podemos ver, parece que estuviéramos llamando un método (o función), y en efecto es algo parecido
u1
es una instancia de la clase Usuario.
También podemos llamar a u1
un objeto.
Agregando propiedades y datos #
Podemos adjuntar datos a este objeto, usamos la notación punto. Esta no es la mejor forma de incluir propiedades a una instancia de clase. De hecho no es posible hacerlo en otros lenguajes. Lo hacemos solamente por ilustración y porque en Python es posible hacerlo.
#Adjuntando datos a el objeto u1 de la clase Usuario
u1.nombre = "Aprendizaje"
u1.apellido = "Profundo"
u2.edad = 15
print(u1.nombre)
print(u1.apellido)
print(u2.edad)
Aprendizaje
Profundo
15
Los datos adjuntados a un objeto se les llaman atributos
Alerta #
Los atributos nombre y apellido no son variables existentes en el espacio de trabajo. Son atributos del objeto u1.
Convenio de notación#
Se recomienda usar minúsculas para los nombres de los atributos (Tradición Pythonica).
Es necesario diferenciar entre atributos de clase y atributos de instancia de clase. Por lo pronto nos referimos a los atributos de instancia de clase que refieren a la información incluida en una instancia de clase. Más adelante revisamos los atributos de clase.
Un atributo de instancia de clase (objeto) se denota mediante la notación nombre_objeto.atributo
. Por ejemplo, hemos creado el atributo (propiedad) nombre para el objeto u1. Si deseamos ver el contenido de tal atributo simplemente escribimos
u1.nombre
'Aprendizaje'
Una bonita consecuencia, es que podemos crear muchos objetos con campos del mismo nombre sin tener que definir una variable diferente para cada dato adjuntado del objeto. Veamos otro objeto:
u2 = Usuario()
u2.nombre = "Francisco"
u2.apellido = "Talavera"
u2.edad = 34
#edad de u2
u2.edad
34
# u1 no tiene edad asginada
u1.edad
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
/tmp/ipykernel_2647/456201229.py in <module>
1 # u1 no tiene edad asginada
----> 2 u1.edad
AttributeError: 'Usuario' object has no attribute 'edad'
Se estarán preguntando…
¿Para qué tomarnos la molestia si pudimos hacer todo con un diccionario?
La respuesta la encontraremos en características adicionales de las clases. Estas contienen:
Métodos
Inicialización
La función __init__()#
Una función dentro de una clase de llama método.
__init__ es el abreviado de initialization (inicialización).
También se le conoce como el constructor.
Note los dos guiones bajos antes y después de init.
class Usuario:
def __init__(self, nombre_completo, fecha_nacimiento):
self.nombre = nombre_completo
self.fecha_nacimiento = fecha_nacimiento
u3 = Usuario("Thomas Anderson", '19620311')
print(u3.nombre)
print(u3.fecha_nacimiento)
Thomas Anderson
19620311
Nota
self es el parámetro que referencia la instancia actual de la clase y se usa para acceder a los atributos de dicha clase. No tiene que llamarse self.
class Usuario:
def __init__(mi_objeto, nombre_completo, fecha_nacimiento):
mi_objeto.nombre = nombre_completo
mi_objeto.fecha_nacimiento = fecha_nacimiento
u3 = Usuario("Thomas Anderson", '19620311')
print(u3.nombre)
print(u3.fecha_nacimiento)
Thomas Anderson
19620311
Agreguemos otra característica más.
Por ejemplo, extraer nombre y apellido:
class Usuario:
def __init__(self, nombre_completo, fecha_nacimiento):
self.nombre_c = nombre_completo
self.fecha_nacimiento = fecha_nacimiento
#Extraer partes
piezas_nombre = nombre_completo.split(" ")
self.nombre = piezas_nombre[0]
self.apellido = piezas_nombre[-1]
u = Usuario("Thomas Anderson", '19620311')
print(u.nombre_c)
print(u.nombre)
print(u.apellido)
print(u.fecha_nacimiento)
Thomas Anderson
Thomas
Anderson
19620311
Documentación de una clase#
Podemos documentar la clase de la siguiente manera:
class Usuario:
"""Un usuario de nuestra plataforma. Por ahora
sólo recolectamos nombre y cumpleaños.
Pero pronto tendremos mucho más que eso."""
def __init__(self, nombre_completo, fecha_nacimiento):
self.nombre_c = nombre_completo
self.fecha_nacimiento = fecha_nacimiento
#Extraer partes
piezas_nombre = nombre_completo.split(" ")
self.nombre = piezas_nombre[0]
self.apellido = piezas_nombre[-1]
help(Usuario)
Help on class Usuario in module __main__:
class Usuario(builtins.object)
| Usuario(nombre_completo, fecha_nacimiento)
|
| Un usuario de nuestra plataforma. Por ahora
| sólo recolectamos nombre y cumpleaños.
| Pero pronto tendremos mucho más que eso.
|
| Methods defined here:
|
| __init__(self, nombre_completo, fecha_nacimiento)
| Initialize self. See help(type(self)) for accurate signature.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
Agregando métodos a una clase#
Es posible crear métodos propios a una clase.
Creemos por ejemplo un método que extraiga la edad de nuestro usuario.
import datetime
class Usuario:
"""Un usuario de nuestra plataforma. Por ahora
sólo recolectamos nombre y cumpleaños.
Pero pronto tendremos mucho más que eso."""
def __init__(self, nombre_completo, fecha_nacimiento):
self.nombre_c = nombre_completo
self.fecha_nacimiento = fecha_nacimiento
#Extraer partes
piezas_nombre = nombre_completo.split(" ")
self.nombre = piezas_nombre[0]
self.apellido = piezas_nombre[-1]
def edad(self):
"""Regresa la edad de nuestro usuario en años."""
hoy = datetime.date.today()
año = int(self.fecha_nacimiento[0:4])
mes = int(self.fecha_nacimiento[4:6])
dia = int(self.fecha_nacimiento[6:8])
fecha_nac = datetime.date(año,mes,dia)
edad_dias = (hoy-fecha_nac).days
edad_años = edad_dias/365
return int(edad_años)
Neo=Usuario("Thomas Anderson","19620311")
print("Neo tiene",Neo.edad(),"años")
Neo tiene 60 años
help(Usuario)
Help on class Usuario in module __main__:
class Usuario(builtins.object)
| Usuario(nombre_completo, fecha_nacimiento)
|
| Un usuario de nuestra plataforma. Por ahora
| sólo recolectamos nombre y cumpleaños.
| Pero pronto tendremos mucho más que eso.
|
| Methods defined here:
|
| __init__(self, nombre_completo, fecha_nacimiento)
| Initialize self. See help(type(self)) for accurate signature.
|
| edad(self)
| Regresa la edad de nuestro usuario en años.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
Imprimiendo contenidos de una clase por defecto#
Para establecer una forma de print por defecto en una clase, puede agregar el método reservado __str__
. Vamos a redefinir nuestra clase para incluirlo.
import datetime
class Usuario:
"""Un usuario de nuestra plataforma. Por ahora
sólo recolectamos nombre y cumpleaños.
Pero pronto tendremos mucho más que eso."""
def __init__(self, nombre_completo, fecha_nacimiento):
self.nombre_c = nombre_completo
self.fecha_nacimiento = fecha_nacimiento
#Extraer partes
piezas_nombre=nombre_completo.split(" ")
self.nombre=piezas_nombre[0]
self.apellido=piezas_nombre[-1]
def edad(self):
"""Regresa la edad de nuestro usuario en años."""
hoy=datetime.date.today()
año=int(self.fecha_nacimiento[0:4])
mes=int(self.fecha_nacimiento[4:6])
dia=int(self.fecha_nacimiento[6:8])
fecha_nac=datetime.date(año,mes,dia)
edad_dias=(hoy-fecha_nac).days
edad_años=edad_dias/365
return int(edad_años)
def __str__(self):
return self.nombre_c + ' tiene ' + str(self.edad()) + ' años'
Neo=Usuario("Thomas Anderson","19620311")
print(Neo)
Thomas Anderson tiene 60 años
Ejecución de una tarea por defecto en una instancia de clase: call#
Puede usar el método reservado __call__
para ejecutar uan tarea por defecto cuando se llama a un objeto.
Por ejemplo supongamos que deseamos ver cuantas veces ha sido llamado un objeto.
Agrandamos una vez más nuestra clase Usuario.
import datetime
class Usuario:
"""Un usuario de nuestra plataforma. Por ahora
sólo recolectamos nombre y cumpleaños.
Pero pronto tendremos mucho más que eso."""
def __init__(self, nombre_completo, fecha_nacimiento):
self.nombre_c = nombre_completo
self.fecha_nacimiento = fecha_nacimiento
self.llamadas = 0
#Extraer partes
piezas_nombre=nombre_completo.split(" ")
self.nombre=piezas_nombre[0]
self.apellido=piezas_nombre[-1]
def edad(self):
"""Regresa la edad de nuestro usuario en años."""
hoy=datetime.date.today()
año=int(self.fecha_nacimiento[0:4])
mes=int(self.fecha_nacimiento[4:6])
dia=int(self.fecha_nacimiento[6:8])
fecha_nac=datetime.date(año,mes,dia)
edad_dias=(hoy-fecha_nac).days
edad_años=edad_dias/365
return int(edad_años)
def __str__(self):
return self.nombre_c + ' tiene ' + str(self.edad()) + ' años'
def __call__(self):
self.llamadas +=1
return(self.llamadas)
Neo=Usuario("Thomas Anderson","19620311")
print(Neo)
print(Neo())
print(Neo())
Thomas Anderson tiene 60 años
1
2
Referencias y copia de objetos#
Smith = Usuario("Agente Smith","20100515")
print(Smith)
Agente Smith tiene 12 años
Ahora una referencia a Neo
px = Neo
print(px)
print(Neo)
Thomas Anderson tiene 62 años
Thomas Anderson tiene 62 años
Pero ahora observe lo que pasa si modifica px
px.fecha_nacimiento ='19500319'
print(Neo)
print(px)
Thomas Anderson tiene 72 años
Thomas Anderson tiene 72 años
Esto ocurre porque en realidad px es una referencia al objeto Neo. Si desea una copia física puede por ejemplo usar la función copy del módulo estándar copy.
from copy import copy
px = copy(Neo)
Neo.fecha_nacimiento = "19600311"
print(px)
print(Neo)
Thomas Anderson tiene 62 años
Thomas Anderson tiene 62 años
Eliminación de objetos: del#
Se usa para eliminar un objeto. Por ejemplo:
del px
print(px)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-26-179114ef99b5> in <module>
1 del px
----> 2 print(px)
NameError: name 'px' is not defined
Atributos Intrínsecos de clases y objetos#
Cada clase y objeto de Python tiene una conjunto de atributos intrínsecos que pueden ser llamados.
Los atributos intrínsicos de clase son:
__name__
: nombre de la clase__module__
: módulo al cual pertenece la clase__bases__
: clases base de ésta clase__dict__
: diccionario conteniendo un conjunto clave/valor con todos los atributos de la clase incluídos los métodos.
Los atributos intrínsicos de los objetos son:
__class__
: nombre de la clase del objeto__dict__
: diccionario conteniendo un conjunto clave/valor con todos los atributos
print('Atributos de clase\n')
print('Nombre de la clase: ',Usuario.__name__)
print('\n Módulo: ', Usuario.__module__)
print('\n Documentación:\n',Usuario.__doc__)
print('\nDiccionario de la clase: \n',Usuario.__dict__)
print('\nClases Base: ',Usuario.__bases__)
print('\nAtributos del objeto Neo\n')
print('Clase: ',Neo.__class__)
print('\n Diccionario: ', Neo.__dict__)
Atributos de clase
Nombre de la clase: Usuario
Módulo: __main__
Documentación:
Un usuario de nuestra plataforma. Por ahora
sólo recolectamos nombre y cumpleaños.
Pero pronto tendremos mucho más que eso.
Diccionario de la clase:
{'__module__': '__main__', '__doc__': 'Un usuario de nuestra plataforma. Por ahora\n sólo recolectamos nombre y cumpleaños.\n Pero pronto tendremos mucho más que eso.', '__init__': <function Usuario.__init__ at 0x7fade03b0160>, 'edad': <function Usuario.edad at 0x7fade03b03a0>, '__str__': <function Usuario.__str__ at 0x7fade03b0430>, '__call__': <function Usuario.__call__ at 0x7fade03b04c0>, '__dict__': <attribute '__dict__' of 'Usuario' objects>, '__weakref__': <attribute '__weakref__' of 'Usuario' objects>, '__slotnames__': []}
Clases Base: (<class 'object'>,)
Atributos del objeto Neo
Clase: <class '__main__.Usuario'>
Diccionario: {'nombre_c': 'Thomas Anderson', 'fecha_nacimiento': '19600311', 'llamadas': 2, 'nombre': 'Thomas', 'apellido': 'Anderson'}
Herencia de clases#
Una de las grandes ventajas de usar clases en programación es poder generar clases más especializadas a partir de una o más clases base.
Esto característica permite re-utilizar código y también permite escribir un código más limpio y legible.
Supongamos que a la clase Usuario le queremos dar un tipo de publicidad en específico.
Creemos entonces una clase que hable sobre los gustos del usuario, pero referenciando a la clase ya creada.
class Lector(Usuario):
pass
help(Lector)
Help on class Lector in module __main__:
class Lector(Usuario)
| Lector(nombre_completo, fecha_nacimiento)
|
| Method resolution order:
| Lector
| Usuario
| builtins.object
|
| Methods inherited from Usuario:
|
| __call__(self)
| Call self as a function.
|
| __init__(self, nombre_completo, fecha_nacimiento)
| Initialize self. See help(type(self)) for accurate signature.
|
| __str__(self)
| Return str(self).
|
| edad(self)
| Regresa la edad de nuestro usuario en años.
|
| ----------------------------------------------------------------------
| Data descriptors inherited from Usuario:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| ----------------------------------------------------------------------
| Data and other attributes inherited from Usuario:
|
| __slotnames__ = []
l=Lector("Daniel Montenegro","19901026")
print(l.nombre_c)
print(l.nombre)
print(l.edad())
print(l)
print(l())
Daniel Montenegro
Daniel
31
Daniel Montenegro tiene 31 años
1
Al intentar colocar un constructor sobre la clase heredada, se perderá la función constructora heredada de Usuario:
class Lector(Usuario):
def __init__(self, nombre_completo, fecha_nacimiento):
self.nombre_c = nombre_completo
self.fecha_nacimiento = fecha_nacimiento
# Agregar cositas
l = Lector("Daniel Montenegro","19901026")
print(l.nombre_c)
print(l.edad())
Daniel Montenegro
31
Referencia a la clase base: super() #
Dado que al heredar una clase de otra, estamos pensando en conservar la funcionalidad de la clase base, es importante poder usar el constructor de la clase base junto con el constructor extendido en la clase heredada. Para hacer esta característica posible se utiliza super()
como se muestra a continuación. super() es un enlace o referencia a la clase base. Entonces, si deseamos usar el constructor de la clase base se puede escribir
super().__init__(…)
Veamos el uso de super() en nuestra clase Lector, la cual heredamos de la clase Usuario.
class Lector(Usuario):
def __init__(self, nombre_completo, fecha_nacimiento, gustos):
super().__init__(nombre_completo, fecha_nacimiento)
self.gustos = gustos
# Agregar otras cosas
l=Lector("Daniel Montenegro","19901026","Jack Kerouac")
print(l.nombre_c)
print(l.edad())
print(l.gustos)
print(l)
Finalmente, agreguemos un método a la clase Lector para extender su funcionalidad:
class Lector(Usuario):
def __init__(self, nombre_completo, fecha_nacimiento, gustos):
super().__init__(nombre_completo, fecha_nacimiento)
self.gustos=gustos
año=int(self.fecha_nacimiento[0:4])
mes=int(self.fecha_nacimiento[4:6])
dia=int(self.fecha_nacimiento[6:8])
self.fecha_nac=datetime.date(año,mes,dia)
def info(self):
print(" El Usuario",self.nombre_c,", nacido en",self.fecha_nac, ", tiene",self.edad(),"años", "y le gustan las obras de",self.gustos)
l = Lector("Daniel Montenegro","19901026","Jack Kerouac")
l.info()
El Usuario Daniel Montenegro , nacido en 1990-10-26 , tiene 31 años y le gustan las obras de Jack Kerouac