Chapter 12
Clases y objetos
12.1 Tipos compuestos definidos por el usuario
Una vez utilizados algunos de los tipos internos de Python, estamos listos
para crear un tipo definido por el usuario: el Punto.
Piense en el concepto de un punto matemático. En dos dimensiones, un punto
es dos números (coordenadas) que se tratan colectivamente como un solo
objeto. En notación matemática, los puntos suelen escribirse entre
paréntesis con una coma separando las coordenadas. Por ejemplo, (0, 0)
representa el origen, y (x, y) representa el punto x unidades a la
derecha e y unidades hacia arriba desde el origen.
Una forma natural de representar un punto en Python es con dos valores en
coma flotante. La cuestión es, entonces, cómo agrupar esos dos valores en
un objeto compuesto. La solución rápida y burda es utilizar una lista o
tupla, y para algunas aplicaciones esa podría ser la mejor opción.
Una alternativa es que el usuario defina un nuevo tipo compuesto, también
llamado una clase. Esta aproximación exige un poco más de esfuerzo,
pero tiene sus ventajas que pronto se harán evidentes.
Una definición de clase se parece a esto:
\begin{verbatim
class Punto:
pass
\end{verbatim}
\afterverb
%
Las definiciones de clase pueden aparecer en cualquier lugar de un
programa, pero normalmente están al principio (tras las sentencias
{\tt import}). Las reglas sintácticas de la definición de clases son
las mismas que para cualesquiera otras sentencias compuestas. (ver la
Sección~\ref{ejecución condicional}).
Esta definición crea una nueva clase llamada {\tt Punto}. La sentencia
{\bf pass} no tiene efectos; sólo es necesaria porque una sentencia
compuesta debe tener algo en su cuerpo.
Al crear la clase {\tt Punto} hemos creado un nuevo tipo, que también
se llama {\tt Punto}. Los miembros de este tipo se llaman {\bf instancias}
del tipo u {\bf objetos}. La creación de una nueva instancia se llama
{\bf instanciación}. Para instanciar un objeto {\tt Punto} ejecutamos una
función que se llama (lo ha adivinado) {\tt Punto}:
\index{instancia!objeto}
\index{instancia de un objeto}
\index{instanciación}
\beforeverb
\begin{verbatim}
limpio = Punto()
\end{verbatim}
\afterverb
%
A la variable {\tt limpio} se le asigna una referencia a un nuevo
objeto {\tt Punto}. A una función como {\tt Punto} que crea un
objeto nuevo se le llama {\bf constructor}.
\section{Atributos}
\index{atributos}
Podemos añadir nuevos datos a una instancia utilizando la notación de punto:
\beforeverb
\begin{verbatim}
>>> limpio.x = 3.0
>>> limpio.y = 4.0
\end{verbatim}
\afterverb
%
Esta sintaxis es similar a la sintaxis para seleccionar una variable de
un módulo, como {\tt math.pi} o {\tt string.uppercase}. En este caso,
sin embargo, estamos seleccionando un dato de una instancia. Estos ítemes
con nombre se llaman {\bf atributos}.
El diagrama de estados que sigue muestra el resultado de esas asignaciones:
\beforefig
\centerline{\psfig{figure=illustrations/point.eps}}
\afterfig
La variable {\tt limpio} apunta a un objeto Punto, que
contiene dos atributos. Cada atributo apunta a un número
en coma flotante.
Podemos leer el valor de un atributo utilizando la misma sintaxis:
\beforeverb
\begin{verbatim}
>>> print limpio.y
4.0
>>> x = limpio.x
>>> print x
3.0
\end{verbatim}
\afterverb
%
La expresión {\tt limpio.x} significa, ``ve al objeto al que apunta {\tt limpio}
y toma el valor de {\tt x}.'' En este caso, asignamos ese valor a una variable
llamada {\tt x}. No hay conflicto entre la variable {\tt x} y el atributo {\tt x}.
El propósito de la notación de punto es identificar de forma inequívoca a qué
variable se refiere.
Puede usted usar la notación de punto como parte de cualquier expresión. Así, las
sentencias que siguen son correctas:
\beforeverb
\begin{verbatim}
print '(' + str(limpio.x) + ', ' + str(limpio.y) + ')'
distanciaAlCuadrado = limpio.x * limpio.x + limpio.y * limpio.y
\end{verbatim}
\afterverb
%
La primera línea presenta {\tt (3.0, 4.0)}; la segunda línea calcula el valor 25.0.
Puede tentarle imprimir el propio valor de {\tt limpio}:
\beforeverb
\begin{verbatim}
>>> print limpio
<__main__.Point instance at 80f8e70>
\end{verbatim}
\afterverb
%
El resultado indica que {\tt limpio} es una instancia de la clase
{\tt Punto} que se definió en {\tt \_\_main\_\_}. {\tt 80f8e70} es
el identificador único de este objeto, escrito en hexadecimal.
Probablemente no es esta la manera más clara de mostrar un objeto
{\tt Punto}. En breve verá cómo cambiarlo.
\index{imprimir!objeto}
\begin{quote}
{\em Como ejercicio, cree e imprima un objeto {\tt Punto} y luego usa
{\tt id} para imprimir el identificador único del objeto. Traduzcael
número hexadecimal a decimal y asegúrese de que coinciden.}
\end{quote}
\section{Instancias como parámetro}
\index{instancia}
\index{parámetro}
Puede usted pasar una instancia como parámetro de la forma habitual.
Por ejemplo:
\beforeverb
\begin{verbatim}
def imprimePunto(p):
print '(' + str(p.x) + ', ' + str(p.y) + ')'
\end{verbatim}
\afterverb
%
{\tt imprimePunto} acepta un punto como argumento y lo muestra
en formato estándar. Si llamas a {\tt imprimePunto(limpio)}, el
resultado es {\tt (3.0, 4.0)}.
\begin{quote}
{\em Como ejercicio, reescriba la función {\tt distancia} de la
Sección~\ref{desarrollo del programa} de forma que acepte dos {\tt Punto}s como
parámetros en lugar de cuatro números.}
\end{quote}
\section{Mismidad}
\index{mismidad}
El significado de la palabra "mismo" parece totalmente claro hasta
que ae para un poco a pensarlo, y entonces se da cuenta de que hay
algo más de lo que suponía.
\index{ambigüedad}
\index{lenguaje natural}
\index{lenguage!}
Por ejemplo, si dice "Pepe y yo tenemos la misma moto", lo que quiere decir
es que su moto y la de usted son de la misma marca y modelo, pero que son dos motos
distintas. Si dice "Pepe y yo tenemos la misma madre", quieres decir que su madre
y la de usted son la misma persona\footnote{No todas las lenguas tienen el mismo
problema. Por ejemplo, el alemán tienen palabras diferentes para los diferentes
tipos de identidad. "Misma moto" en este contexto sería "gleiche Motorrad" y "misma
madre" sería "selbe Mutter".}. Así que la idea de "identidad" es diferente según
el contexto.
Cuando habla de objetos, hay una ambigüedad parecida. Por ejemplo,
si dos {\tt Punto}s son el mismo, ¿significa que contienen los mismos datos
(coordenadas) o que son de verdad el mismo objeto?
Para averiguar si dos referencias se refieren al mismo objeto, utilice el
operador {\tt ==}. Por ejemplo:
\beforeverb
\begin{verbatim}
>>> p1 = Punto()
>>> p1.x = 3
>>> p1.y = 4
>>> p2 = Puto()
>>> p2.x = 3
>>> p2.y = 4
>>> p1 == p2
0
\end{verbatim}
\afterverb
%
Aunque {\tt p1} y {\tt p2} contienen las mismas coordenadas, no son
el mismo objeto. Si asignamos {\tt p1} a {\tt p2}, las dos variables son
alias del mismo objeto:
\beforeverb
\begin{verbatim}
>>> p2 = p1
>>> p1 == p2
1
\end{verbatim}
\afterverb
%
Este tipo de igualdad se llama {\bf igualdad superficial} porque sólo
compara las referencias, pero no el contenido de los objetos.
\index{igualdad}
\index{identidad}
\index{igualdad superficial}
\index{igualdad profunda}
Para comparar los contenidos de los objetos ({\bf igualdad profunda}) podemos
escribir una función llamada {\tt mismoPunto}:
\beforeverb
\begin{verbatim}
def mismoPunto(p1, p2) :
return (p1.x == p2.x) and (p1.y == p2.y)
\end{verbatim}
\afterverb
%
Si ahora creamos dos objetos diferentes que contienen los mismos datos
podremos usar {\tt mismoPunto} para averiguar si representan el mismo punto:
\beforeverb
\begin{verbatim}
>>> p1 = Punto()
>>> p1.x = 3
>>> p1.y = 4
>>> p2 = Punto()
>>> p2.x = 3
>>> p2.y = 4
>>> mismoPunto(p1, p2)
1
\end{verbatim}
\afterverb
%
Por supuesto, si las dos variables apuntan al mismo objeto {\tt mismoPunto}
devuelve verdadero.
\section{Rectángulos}
\index{rectángulo}
Digamos que queremos una clase que represente un rectángulo. La pregunta es,
¿qué información tenemos que proporcionar para definir un rectángulo? Para
simplificar las cosas, supongamos que el rectángulo está orientado vertical u
horizontalmente, nunca en diagonal.
Tenemos varias posibilidades: podemos señalar el centro del rectángulo
(dos coordenadas) y su tamaño (anchura y altura); o podemos señalar una de
las esquinas y el tamaño; o podemos señalar dos esquinas opuestas. Un modo
convencional es señalar la esquina superior izquierda del rectángulo y el tamaño.
De nuevo, definiremos una nueva clase:
\beforeverb
\begin{verbatim}
class Rectangulo: # ¡Prohibidos los acentos fuera de las cadenas!
pass
\end{verbatim}
\afterverb
%
Y la instanciaremos:
\beforeverb
\begin{verbatim}
caja = Rectangulo()
caja.anchura = 100.0
caja.altura = 200.0
\end{verbatim}
\afterverb
%
Este código crea un nuevo objeto {\tt Rectangulo} con dos atributos en coma flotante.
¡Para señalar la esquina superior izquierda podemos incrustar un objeto dentro de otro!
\beforeverb
\begin{verbatim}
caja.esquina = Punto()
caja.esquina.x = 0.0;
caja.esquina.y = 0.0;
\end{verbatim}
\afterverb
%
El operador punto compone. La expresión {\tt caja.esquina.x} significa
"ve al objeto al que se refiere {\tt caja} y selecciona el atributo llamado
{\tt esquina}; entonces ve a ese objeto y selecciona el atributo llamado
{\tt x}."
La figura muestra el estado de este objeto:
\beforefig
\centerline{\psfig{figure=illustrations/rectangle.eps}}
\afterfig
\section{Instancias como valores de retorno}
\index{instancia}
\index{valor de retorno}
Las funciones pueden devolver instancias. Por ejemplo, {\tt encuentraCentro}
acepta un {\tt Rectangulo} como argumento y devuelve un {\tt Punto} que
contiene las coordenadas del centro del {\tt Rectangulo}:
\beforeverb
\begin{verbatim}
def encuentraCentro(caja):
p = Punto()
p.x = caja.esquina.x + caja.anchura/2.0
p.y = caja.esquina.y + caja.altura/2.0
return p
\end{verbatim}
\afterverb
%
Para llamar a esta fucnión, pase {\tt caja} como argumento y asigne el resultado
a una variable:
\beforeverb
\begin{verbatim}
>>> centro = encuentraCentro(caja)
>>> imprimePunto(centro)
(50.0, 100.0)
\end{verbatim}
\afterverb
%
\section{Los objetos son mudables}
\index{objeto!mudable}
\index{objeto mudable}
Podemos cambiar el estado de un objeto efectuando una asignación
sobre uno de sus atributos. Por ejemplo, para cambiar el tamaño de
un rectángulo sin cambiar su posición, podemos cambiar los valores de
{\tt anchura} y {\tt altura}:
\beforeverb
\begin{verbatim}
caja.anchura = caja.anchura + 50
caja.altura = caja.altura + 100
\end{verbatim}
\afterverb
%
Podemos encapsular este código en un método y generalizarlo
para agrandar el rectángulo en cualquier cantidad:
\index{encapsulación}
\index{generalización}
\beforeverb
\begin{verbatim}
def agrandarRect(caja, danchura, daltura) :
caja.anchura = caja.anchura + danchura
caja.altura = caja.altura + daltura
\end{verbatim}
\afterverb
%
Las variables {\tt danchura} y {\tt daltura} indican cuánto debe agrandarse
el rectángulo en cada dirección. Invocar este método tiene el efecto de
modificar el {\tt Rectangulo} que se pasa como argumento.
Por ejemplo, podemos crear un nuevo {\tt Rectangulo} llamado {\tt bob}
y pasárselo a {\tt agrandaRect}:
\beforeverb
\begin{verbatim}
>>> bob = Rectangulo()
>>> bob.anchura = 100.0
>>> bob.altura = 200.0
>>> bob.esquina = Punto()
>>> bob.esquina.x = 0.0;
>>> bob.esquina.y = 0.0;
>>> agrandaRect(bob, 50, 100)
\end{verbatim}
\afterverb
%
Mientras {\tt agrandaRect} se está ejecutando, el parámetro {\tt caja} es un
alias de {\tt bob}. Cualquier cambio que haga a {\tt caja} afectará también a
{\tt bob}.
\begin{quote}
{\em A modo de ejercicio, escriba una función llamada {\tt mueveRect} que tome
un {\tt Rectangulo} y dos parámetros llamados {\tt dx} y {\tt dy}. Tiene que cambiar
la posición del rectángulo añadiendo {\tt dx} a la coordenada {\tt x} de
{\tt esquina} y añadiendo {\tt dy} a la coordenada {\tt y} de {\tt esquina}.}
\end{quote}
\section{Copiado}
\index{uso de alias}
\index{copiado}
\index{módulo copy}
\index{módulo!copy}
El uso de alias puede hacer que un programa sea difícil de leer,
porque los cambios hechos en un lugar pueden tener efectos inesperados
en otro lugar. Es difícil estar al tanto de todas las variables a las
que puede apuntar un objeto dado.
Copiar un objeto es, muchas veces, una alternativa a la creación de un alias.
El módulo {\tt copy} contiene una función llamada {\tt copy} que puede
duplicar cualquier objeto:
\beforeverb
\begin{verbatim}
>>> import copy
>>> p1 = Punto()
>>> p1.x = 3
>>> p1.y = 4
>>> p2 = copy.copy(p1)
>>> p1 == p2
0
>>> mismoPunto(p1, p2)
1
\end{verbatim}
\afterverb
%
Una vez que hemos importado el módulo {\tt copy}, podemos usar el método {\tt copy}
para hacer un nuevo {\tt Punto}. {\tt p1} y {\tt p2} no son el mismo punto, pero
contienen los mismos datos.
Para copiar un objeto simple como un {\tt Punto}, que no contiene objetos incrustados,
{\tt copy} es suficiente. Esto se llama {\bf copiado superficial}.
Para algo como un {\tt Rectangulo}, que contiene una referencia a un
{\tt Punto}, {\tt copy} no lo hace del todo bien. Copia la referencia al objeto
{\tt Punto}, de modo que tanto el {\tt Rectangulo} viejo como el nuevo apuntan a
un único {\tt Punto}.
Si creamos una caja, {\tt b1}, de la forma habitual y entonces hacemos una
copia, {\tt b2}, usando {\tt copy}, el diagrama de estados resultante se ve
así:
\beforefig
\centerline{\psfig{figure=illustrations/rectangle2.eps}}
\afterfig
Es casi seguro que esto no es lo que queremos. En este caso, la invocación
de {\tt agrandaRect} sobre uno de los {\tt Rectangulo}s no afectaría al otro,
¡pero la invocación de {\tt mueveRect} sobre cualquiera afectaria a ambos!
Este comportamiento es confuso y propicia los errores.
Afortunadamente, el módulo {\tt copy} contiene un método llamado {\tt
deepcopy} que copia no sólo el objeto sino también cualesquiera objetos
incrustados. No le sorprenderá saber que esta operación se llama {\bf
copia profunda} (deep copy).
\beforeverb
\begin{verbatim}
>>> b2 = copy.deepcopy(b1)
\end{verbatim}
\afterverb
%
Ahora {\tt b1} y {\tt b2} son objetos totalmente independientes.
Podemos usar {\tt deepcopy} para reescribir {\tt agrandaRect} de modo que
en lugar de modificar un {\tt Rectangulo} existente, cree un nuevo
{\tt Rectangulo} que tiene la misma localización que el viejo pero nuevas
dimensiones:
\beforeverb
\begin{verbatim}
def agrandaRect(caja, danchura, daltura) :
import copy
nuevaCaja = copy.deepcopy(caja)
nuevaCaja.anchura = neuvaCaja.anchura + danchura
nuevaCaja.altura = nuevaCaja.altura + daltura
return nuevaCaja
\end{verbatim}
\afterverb
%
\begin{quote}
{\em Como ejercicio, resscriba {\tt mueveRect} de modo que cree y devuelva
un nuevo {\tt Rectangulo} en lugar de modificar el viejo.}
\end{quote}
\section{Glosario}
\begin{description}
\item[clase:] Un tipo compuesto definido por el usuario. También se puede pensar en una
clase como una plantilla para los objetos que son instancias de la misma.
\item[instanciar:] Crear una instancia de una clase.
\item[instancia:] Un objeto que pertenece a una clase.
\item[objeto:] Un tipo de dato compuesto que suele usarse para representar
una cosa o concepto del mundo real.
\item[constructor:] Un método usado para crear nuevos objetos.
\item[atributo:] Uno de los elementos de datos con nombre que
constituyen una instancia.
\item[igualdad superficial:] Igualdad de referencias, o dos referencias
que apuntan al mismo objeto.
\item[igualdad profunda:] Igualdad de valores, o dos referencias que apuntan
a objetos que tienen el mismo valor.
\item[copia superficial:] Copiar el contenido de un objeto, incluyendo
cualquier referencia a objetos incrustados; implementada por la función {\tt copy}
del módulo {\tt copy}.
\item[copia profunda:] Copiar el contenido de un objeto así como cualesquiera
objetos incrustados, y los incrustados en estos, y así; implementada por la
función {\tt deepcopy} del módulo {\tt copy}.
\index{clase}
\index{instanciar}
\index{instancia}
\index{objeto}
\index{constructor}
\index{atributo}
\index{igualdad superficial}
\index{igualdad profunda}
\index{copia superficial}
\index{copia profunda}
\end{description}