Al comienzo de este libro, vimos cuatro patrones básicos de programación que utilizamos para construir programas:
if
)En capítulos posteriores, exploramos las variables simples, así como estructuras de datos de colecciones, tales como listas, tuplas y diccionarios.
A medida que construimos programas, diseñamos estructuras de datos y escribimos código para manipularlas. Hay muchas formas de escribir programas y, a estas alturas, probablemente hayas escrito algunos programas “no muy elegantes” y otros que son “más elegantes”. Aunque tus programas aún sean pequeños, estás empezando a ver que al escribir código hay una parte de arte y de consideraciones estéticas.
A medida que los programas crecen hasta abarcar millones de líneas de código, se vuelve cada vez más importante que éste resulte fácil de entender. Si estás trabajando en un programa de un millón de líneas, es imposible mantener todo el programa en tu mente a la vez. Necesitamos maneras de dividir programas grandes en varias piezas más pequeñas para que tengamos que concentrarnos en una sección menor cuando tengamos que resolver un problema, arreglar un bug, o agregar una nueva funcionalidad.
En cierto modo, la programación orientada a objetos es una forma de ordenar tu código de tal manera que puedas enfocarte en 50 líneas de código y entenderlas, e ignorar las otras 999,950 mientras tanto.
Al igual que muchos aspectos de la programación, es necesario aprender los conceptos de la programación orientada a objetos para poder utilizarlos de manera efectiva. Deberías enfocarte en este capítulo como una forma de aprender algunos términos y conceptos y examinar algunos ejemplos sencillos para sentar las bases de tu futuro aprendizaje.
El resultado clave de este capítulo será una comprensión a nivel básico de cómo se construyen los objetos, cómo funcionan y, lo más importante, cómo usar las características de los objetos que nos dan Python y sus librerías.
Curiosamente, en el libro hemos estado utilizando objetos todo este tiempo. Python contiene muchos objetos incluidos. He aquí un programa sencillo, cuyas primeras líneas deberían resultarte sumamente simples y familiares.
cosa = list()
cosa.append('python')
cosa.append('chuck')
cosa.sort()
print (cosa[0])
print (cosa.__getitem__(0))
print (list.__getitem__(cosa,0))
# Código: https://es.py4e.com/code3/party1.py
En lugar de enfocarnos en el resultado que obtienen estas líneas, enfoquémonos en lo que está pasando desde el punto de vista de la programación orientada a objetos. No te preocupes si los siguientes párrafos no parecen tener sentido la primera vez que los lees, pues no hemos definido todos estos términos aún.
La primera línea construye un objeto de tipo list
, la segunda y tercera líneas llaman al método append
, la cuarta línea llama al método sort()
y la quinta línea recupera el elemento en posición 0.
La sexta línea llama al método __getitem__()
en la lista cosa
con un parámetro de cero.
print (cosa.__getitem__(0))
La séptima línea es una manera incluso más verbosa de obtener el elemento en posición 0 de la lista.
print (list.__getitem__(cosa,0))
En este programa, llamamos al método __getitem__
en la clase lista
y pasamos la lista y el elemento que queremos recuperar de ésta como parámetros.
Las últimas tres líneas del programa son equivalentes, pero es más conveniente simplemente usar corchetes para buscar un elemento en una posición específica dentro de una lista.
Podemos ver las capacidades de un objeto mirando el resultado de la función dir()
:
>>> cosa = list()
>>> dir(cosa)
['__add__', '__class__', '__contains__', '__delattr__',
'__delitem__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__getitem__',
'__gt__', '__hash__', '__iadd__', '__imul__', '__init__',
'__iter__', '__le__', '__len__', '__lt__', '__mul__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__reversed__', '__rmul__', '__setattr__',
'__setitem__', '__sizeof__', '__str__', '__subclasshook__',
'append', 'clear', 'copy', 'count', 'extend', 'index',
'insert', 'pop', 'remove', 'reverse', 'sort']
>>>
El resto de este capítulo estará dedicado a definir estos términos, así que asegúrate de volver a leer los párrafos anteriores una vez que termines de leerlo, para asegurarte de haberlo comprendido correctamente.
En su forma más básica, un programa toma algún dato de entrada, lo procesa y luego produce un resultado. Nuestro programa de conversión de un elevador muestra una manera muy corta, pero completa, de llevar a cabo estos tres pasos.
usf = input('Ingresa el Número de Piso US: ')
wf = int(usf) - 1
print('Número de Piso No-US es',wf)
# Código: https://es.py4e.com/code3/elev.py
Si pensamos un poco más sobre este programa, existe un “mundo exterior” y el programa. Es con los datos de entrada y de salida que el programa interactúa con el mundo exterior. Dentro del programa utilizamos tanto el código como los datos para cumplir la tarea que el programa está diseñado para resolver.
Una forma de pensar en la programación orientada a objetos es que separa nuestro programa en varias “zonas”. Cada zona contiene algo de código y datos (como un programa) y tiene interacciones bien definidas tanto con el mundo exterior como con las otras zonas del programa.
Si miramos la aplicación de extracción de enlaces en la que usamos la librería BeautifulSoup, podemos ver un programa que fue construido conectando distintos objetos para cumplir una tarea:
# Para ejecutar este programa descarga BeautifulSoup
# https://pypi.python.org/pypi/beautifulsoup4
# O descarga el archivo
# http://www.py4e.com/code3/bs4.zip
# y descomprimelo en el mismo directorio que este archivo
import urllib.request, urllib.parse, urllib.error
from bs4 import BeautifulSoup
import ssl
# Ignorar errores de certificado SSL
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
url = input('Introduzca - ')
html = urllib.request.urlopen(url, context=ctx).read()
sopa = BeautifulSoup(html, 'html.parser')
# Recuperar todas las etiquetas de anclaje
etiquetas = sopa('a')
for etiqueta in etiquetas:
print(etiqueta.get('href', None))
# Código: https://es.py4e.com/code3/urllinks.py
Convertimos la URL en una cadena y luego pasamos a ésta por urllib
para recuperar los datos de la web. La librería urllib
utiliza la librería socket
para llevar a cabo la conexión que recupera los datos. Tomamos la cadena que retorna urllib
y se la entregamos a BeautifulSoup para su análisis. BeautifulSoup utiliza el objeto html.parser
1 y retorna un objeto. Luego, llamamos al método tags()
en el objeto retornado, lo que retorna un diccionario de etiquetas. Nos desplazamos por las etiquetas y llamamos el método get()
por cada etiqueta para imprimir el atributo href
.
Podemos hacer un diagrama de este programa y cómo los objetos funcionan en conjunto.
Lo importante ahora no es entender perfectamente como funciona este programa, sino que ver cómo construimos una red de objetos que interactúen entre sí y orquestamos el movimiento de información entre esos objetos para crear un programa. También es importante notar que, cuando viste el programa hace varios capítulos, pudiste entender perfectamente cómo funcionaba sin siquiera percatarte de que estaba “orquestando el movimiento de datos entre objetos”. Eran solo líneas de código que cumplían una tarea.
Una de las ventajas del enfoque orientado a objetos es que puede ocultar la complejidad de un programa. Por ejemplo, aunque necesitamos saber cómo usar el código de urllib
y BeautifulSoup, no necesitamos saber cómo funcionan internamente esas librerías. Esto nos permite enfocarnos en la parte del problema que necesitamos resolver e ignorar las otras partes del programa.
Esta capacidad de enfocarnos exclusivamente en la parte del programa que nos preocupa e ignorar el resto también le sirve a los desarrolladores de los objetos que utilizamos. Por ejemplo, los programadores que desarrollan BeautifulSoup no necesitan saber cómo recuperamos nuestra página HTML, qué partes de ésta queremos leer, o qué queremos hacer con los datos que obtengamos de la página web.
En un nivel elemental, un objeto es simplemente un trozo de código más estructuras de datos, más pequeños que un programa completo. Definir una función nos permite almacenar un trozo de código, darle un nombre y luego invocarlo usando el nombre de la función.
Un objeto puede contener varias funciones (a las que llamaremos métodos), así como los datos utilizados por esas funciones. Llamamos atributos a los datos que son parte del objeto.
Usamos la palabra clave class
para definir los datos y el código que compondrán cada objeto. La palabra clave class incluye el nombre de la clase y da inicio a un bloque de código indentado en el que incluiremos sus atributos (datos) y métodos (código).
class GrupoAnimal:
x = 0
def grupo(self) :
self.x = self.x + 1
print("Hasta ahora",self.x)
an = GrupoAnimal()
an.grupo()
an.grupo()
an.grupo()
GrupoAnimal.grupo(an)
# Código: https://es.py4e.com/code3/party2.py
Cada método parece una función: comienzan con la palabra clave def
y consisten en un bloque de código indentado. Este objeto tiene un atributo (x
) y un método (grupo
). Los métodos tienen un primer parámetro especial al que, por convención, llamamos self
.
Tal como la palabra clave def
no causa que el código de una función se ejecute, la palabra clave class
no crea un objeto. En vez, la palabra clave class
define una plantilla que indica qué datos y código contendrá cada objeto de tipo GrupoAnimal
. La clase es como un molde para galletas y los objetos creados usándola son las galletas2. No se le echa el glaseado al molde de las galletas; se le echa glaseado a las galletas mismas, con lo que se puede poner un glaseado distinto en cada galleta.
Si seguimos con este programa de ejemplo, veremos la primera línea ejecutable de código:
an = GrupoAnimal()
Es aquí que le ordenamos a Python construir (es decir, crear) un objeto o instancia de la clase GrupoAnimal
. Se ve como si fuera una llamada de función a la clase misma. Python construye el objeto con los datos y métodos adecuados, asignándolo luego a la variable an
. En cierto modo, esto es muy similar a la siguiente línea, que hemos estado usando todo este tiempo:
counts = dict()
Aquí le ordenamos a Python construir un objeto usando la plantilla dict
(que viene incluida en Python), devolver la instancia del diccionario, y asignarla a la variable counts
.
Cuando se usa la clase GrupoAnimal
para construir un objeto, la variable an
se usa para señalar ese objeto. Usamos an
para acceder al código y datos de esa instancia específica de la clase GrupoAnimal
.
Cada objeto o instancia de GrupoAnimal contiene una variable x
y un método/ función llamado grupo
. Llamamos al método grupo
en esta línea:
an.grupo()
Al llamar al método grupo
, el primer parámetro (al que por convención llamamos self
) apunta a la instancia específica del objeto GrupoAnimal desde el que se llama a grupo
. Dentro del método grupo
, vemos la siguiente línea:
self.x = self.x + 1
Esta sintaxis utiliza el operador de punto, con lo que significa ‘la x dentro de self’. Cada vez que se llama a grupo()
, el valor interno de x
se incrementa en 1 y se imprime su valor.
La siguiente línea muestra otra manera de llamar al método grupo
dentro del objeto an
:
GrupoAnimal.grupo(an)
En esta variante, accedemos al código desde el interior de la clase y explícitamente pasamos el apuntador del objeto an
como el primer parámetro (es decir, self
dentro del método). Se puede pensar en an.grupo()
como una abreviación de la línea precedente.
Al ejecutar el programa, produce el siguiente resultado:
So far 1
So far 2
So far 3
So far 4
El objeto es construido, y el método grupo
es llamado cuatro veces, incrementando e imprimiendo el valor de x
dentro del objeto an
.
Como hemos visto, en Python todas las variables tienen un tipo. Podemos usar la función dir
incluida en Python para examinar las características de una variable. También podemos usar type
y dir
con las clases que creemos.
class GrupoAnimal:
x = 0
def grupo(self) :
self.x = self.x + 1
print("Hasta ahora",self.x)
an = GrupoAnimal()
print ("Type", type(an))
print ("Dir ", dir(an))
print ("Type", type(an.x))
print ("Type", type(an.grupo))
# Código: https://es.py4e.com/code3/party3.py
Al ejecutar este programa, produce el siguiente resultado:
Type <class '__main__.GrupoAnimal'>
Dir ['__class__', '__delattr__', ...
'__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'grupo', 'x']
Type <class 'int'>
Type <class 'method'>
Puedes ver que, usando la palabra clave class
, hemos creado un nuevo tipo. En el resultado de usar dir
, puedes ver que tanto el atributo de tipo entero x
como el método grupo
están disponibles dentro del objeto.
En los ejemplos anteriores, definimos una clase (plantilla), la usamos para crear una instancia de ella (objeto) y luego usamos esa instancia. Al finalizar el programa, todas las variables son descartadas. Normalmente, no nos preocupamos mucho de la creación y destrucción de variables, pero a menudo, cuando nuestros objetos se vuelven más complejos, resulta necesario efectuar algunos pasos dentro del objeto para configurar la construcción de éste y, posiblemente, ordenar cuando el objeto es descartado.
Si queremos que nuestro objeto sea consciente de esos momentos de creación y destrucción, debemos agregarle métodos especialmente nombrados al efecto:
class GrupoAnimal:
x = 0
def __init__(self):
print('Estoy construido')
def grupo(self) :
self.x = self.x + 1
print('Hasta ahora',self.x)
def __del__(self):
print('Estoy destruido', self.x)
an = GrupoAnimal()
an.grupo()
an.grupo()
an = 42
print('an contiene',an)
# Código: https://es.py4e.com/code3/party4.py
Al ejecutar este programa, produce el siguiente resultado:
Estoy construido
Hasta ahora 1
Hasta ahora 2
Estoy destruido 2
an contiene 42
Cuando Python construye el objeto, llama a nuestro método __init__
para darnos la oportunidad de configurar algunos valores por defecto o iniciales para éste. Cuando Python encuentra la línea:
an = 42
efectivamente “tira a la basura” el objeto para reutilizar la variable an
, almacenando el valor 42
. Justo en el momento en que nuestro objeto an
está siendo “destruido” se llama a nuestro código destructor (__del__
). No podemos evitar que nuestra variable sea destruida, pero podemos efectuar la configuración que resulte necesaria antes de que el objeto deje de existir.
Al desarrollar objetos, es bastante común agregarles un constructor que fije sus valores iniciales. Es relativamente raro necesitar un destructor para un objeto.
Hasta ahora hemos definido una clase, construido un solo objeto, usado ese objeto, y luego descartado el objeto. Sin embargo, el auténtico potencial de la programación orientada a objetos se manifiesta al construir múltiples instancias de nuestra clase.
Al construir múltiples instancias de nuestra clase, puede que queramos fijar distintos valores iniciales para cada objeto. Podemos pasar datos a los constructores para dar a cada objeto un distinto valor inicial:
class GrupoAnimal:
x = 0
nombre = ''
def __init__(self, nom):
self.nombre = nom
print(self.nombre,'construido')
def grupo(self) :
self.x = self.x + 1
print(self.nombre,'recuento grupal',self.x)
s = GrupoAnimal('Sally')
j = GrupoAnimal('Jim')
s.grupo()
j.grupo()
s.grupo()
# Código: https://es.py4e.com/code3/party5.py
El constructor tiene tanto un parámetro self
, que apunta a la instancia del objeto, como parámetros adicionales, que se pasan al constructor al momento de construir el objeto:
s = GrupoAnimal('Sally')
Dentro del constructor, la segunda línea copia el parámetro (nom
), el que se pasa al atributo nombre
dentro del objeto.
self.nombre = nom
El resultado del programa muestra que cada objeto (s
y j
) contienen sus propias copias independientes de x
y nom
:
Sally construido
Jim construido
Sally recuento grupal 1
Jim recuento grupal 1
Sally recuento grupal 2
Otra poderosa característica de la programación orientada a objetos es la capacidad de crear una nueva clase extendiendo una clase ya existente. Al extender una clase, llamamos a la clase original la clase padre y a la nueva clase clase hija.
Por ejemplo, podemos mover a nuestra clase GrupoAnimal
a su propio archivo. Luego, podemos ‘importar’ la clase GrupoAnimal
en un nuevo archivo y extenderla, de la siguiente manera:
from party import GrupoAnimal
class GrilloFan(GrupoAnimal):
puntos = 0
def seis(self):
self.puntos = self.puntos + 6
self.grupo()
print(self.nombre,"puntos",self.puntos)
s = GrupoAnimal("Sally")
s.grupo()
j = GrilloFan("Jim")
j.grupo()
j.seis()
print(dir(j))
# Código: https://es.py4e.com/code3/party6.py
Cuando definimos la clase GrilloFan
, indicamos que estamos extendiendo la clase GrupoAnimal
. Esto significa que todas las variables (x
) y métodos (grupo
) de la clase GrupoAnimal
son heredados por la clase GrilloFan
. Por ejemplo, dentro del método six
en la clase GrilloFan
, llamamos al método grupo
de la clase GrupoAnimal
.
Al ejecutar el programa, creamos s
y j
como instancias independientes de GrupoAnimal
y GrilloFan
. El objeto j
tiene características adicionales que van más allá de aquellas que tiene el objeto s
.
Sally construido
Sally recuento grupal 1
Jim construido
Jim recuento grupal 1
Jim recuento grupal 2
Jim puntos 6
['__class__', '__delattr__', ... '__weakref__',
'nombre', 'grupo', 'puntos', 'seis', 'x']
En el resultado de llamar a dir
sobre el objeto j
(instancia de la clase GrilloFan
), vemos que tiene los atributos y métodos de la clase padre, además de los atributos y métodos que fueron agregados cuando la extendimos para crear la clase GrilloFan
.
Esta es una introducción muy superficial a la programación orientada a objetos, enfocada principalmente en la terminología y sintaxis necesarias para definir y usar objetos. Vamos a reseñar rápidamente el código que vimos al comienzo del capítulo. A estas alturas deberías entender completamente lo que está pasando.
cosa = list()
cosa.append('python')
cosa.append('chuck')
cosa.sort()
print (cosa[0])
print (cosa.__getitem__(0))
print (list.__getitem__(cosa,0))
# Código: https://es.py4e.com/code3/party1.py
La primera línea construye un objeto de clase list
. Cuando Python crea el objeto de clase list
llama al método constructor (llamado __init__
) para configurar los atributos internos de datos que se utilizarán para almacenar los datos de la lista. Aún no hemos pasado ningún parámetro al constructor. When el constructor retorna, usamos la variable cosa
para apuntar la instancia retornada de la clase list
.
La segunda y tercera líneas llaman al método append
con un parámetro para agregar un nuevo objeto al final de la lista actualizando los atributos al interior de cosa
. Luego, en la cuarta línea, llamamos al método sort
sin darle ningún parámetro para ordenar los datos dentro del objeto cosa
.
Luego, imprimimos el primer objeto en la lista usando los corchetes, los que son una abreviatura para llamar el método __getitem__
dentro de cosa
. Esto es equivalente a llamado al método __getitem__
dentro de la clase list
y pasar el objeto cosa
como primer parámetro y la posición que necesitamos como segundo parámetro.
Al final del programa, el objeto cosa
es descartado, pero no antes de llamar al método destructor (llamado __del__
) de manera tal que el objeto pueda atar cabos sueltos en caso de resultar necesario.
Estos son los aspectos básicos de la programación orientada a objetos. Hay muchos detalles adicionales sobre cómo usar un enfoque de programación orientada a objetos al desarrollar aplicaciones, así como librerías, las que van más allá del ámbito de este capítulo.3
__init__
) que es llamado al momento en que se utiliza una clase para construir un objeto. Normalmente se utiliza para determinar los valores iniciales del objeto.
__del__
) que es llamado justo un momento antes de que un objeto sea destruido. Los destructores rara vez son utilizados.
https://docs.python.org/3/library/html.parser.html↩︎
Cookie image copyright CC-BY https://www.flickr.com/photos/dinnerseries/23570475099↩︎
Si quieres saber donde se encuentra definida la clase list
, echa un vistazo a (ojalá la URL no cambie) https://github.com/python/cpython/blob/master/Objects/listobject.c - la clase list está escrita en un lenguaje llamado “C”. Si ves el código fuente y sientes curiosidad, quizá te convenga buscar algunos cursos sobre Ciencias de la Computación.↩︎
Si encuentras un error en este libro, siéntete libre de enviarme una solución usando Github.