Programación Orientada a Objetos

Manejando programas más grandes

Al comienzo de este libro, vimos cuatro patrones básicos de programación que utilizamos para construir programas:

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.

Cómo empezar

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.

Usando objetos

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.

Comenzando con programas

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.

A Program

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.parser1 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.

A Program as Network of Objects

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.

Subdividiendo un problema

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.

Ignoring Detail When Using an Object

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.

Ignoring Detail When Building an Object

Nuestro primer objeto de Python

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.

A Class and Two Objects

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.

Clases como tipos

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.

Ciclo de vida de un 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.

Múltiples instancias

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

Herencia

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.

Resumen

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

Glosario

atributo
Una variable que es parte de una clase.
clase
Una plantilla que puede usarse para construir un objeto. Define los atributos y métodos que formarán a dicho objeto.
clase hija
Una nueva clase creada cuando una clase padre es extendida. La clase hija hereda todos los atributos y métodos de la clase padre.
clase padre
La clase que está siendo extendida para crear una nueva clase hija. La clase padre aporta todos sus métodos y atributos a la nueva clase hija.
constructor
Un método opcional con un nombre especial (__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.
destructor
Un método opcional con un nombre especial (__del__) que es llamado justo un momento antes de que un objeto sea destruido. Los destructores rara vez son utilizados.
herencia
Cuando creamos una nueva clase (hija) extendiendo una clase existente (padre). La clase hija tiene todos los atributos y métodos de la clase padre, más los atributos y métodos adicionales definidos por la clase hija.

método
Una función contenida dentro de una clase y de los objetos construidos desde esa clase. Algunos patrones de diseño orientados a objetos describen este concepto como ‘mensaje’ en lugar de ‘método’.
objeto
Una instancia construida de una clase. Un objeto contiene todos los atributos y métodos definidos por la clase. En algunos casos de documentación orientada a objetos se utiliza el término ‘instancia’ de manera intercambiable con ‘objeto’.

  1. https://docs.python.org/3/library/html.parser.html↩︎

  2. Cookie image copyright CC-BY https://www.flickr.com/photos/dinnerseries/23570475099↩︎

  3. 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.