Next Up Previous Hi Index

Chapter 14

Clases y métodos

14.1 Características de la orientación a objetos

Python es un lenguaje de programación orientado a objetos, lo que significa que porporciona características que apoyan la programación orientada a objetos.

No es fácil definir la programación orientada a objetos, pero ya hemos visto algunas de sus características:

Por ejemplo, la clase Hora definida en el Capítulo 13 se corresponde con la forma en la que la gente registra la hora del día, y las funciones que definimos se corresponden con el tipo de cosas que la gente hace con las horas. De forma similar, las clases Puntoy Rectangulo se corresponden con los conceptos matemáticos de un punto y un rectángulo.

Hasta ahora, no nos hemos aprovechado de las características que Python nos ofrece para dar soporte a la programación orientada a objetos. Hablando estrictamente, estas características no son necesarias. En su mayoría, proporcionan una sintaxis alternativa para cosas que ya hemos hecho, pero en muchos casos, la alternativa es más concisa y expresa con más precisión a la estructura del programa.

Por ejemplo, en el programa Hora no hay una conexión obvia entre la definición de la clase y las definiciones de las funciones que siguen. Observando bien, se hace patente que todas esas funciones toman al menos un objeto Hora como parámetro.

Esta observación es la que motiva los métodos. Ya hemos visto varios métodos, como keys y values, que se invocan sobre diccionarios. Cada método está asociado con una clase y está pensado para invocarse sobre instancias de esa clase.

Los métodos son como las funciones, con dos diferencias:

En las próximas secciones tomaremos las funciones de los capítulos anteriores y las transformaremos en métodos. Esta transformación es puramente mecánica; puede hacerla simplemente siguiendo una secuencia de pasos. Si se acostumbra a convertir de una forma a la otra será capaz de elegir la mejor forma de hacer lo que quiere.

14.2 imprimeHora

En el Capítulo 13, definimos una clase llamada Hora y escribimos una fución llamada imprimeHora, que debería ser parecida a esto:

class Hora:
  pass

def
imprimeHora(hora):
  print str(hora.horas) + ":" +
        str(hora.minutos) + ":" +
        str(hora.segundos)

Para llamar a esta función, pasábamos un objeto Hora como parámetro:

>>> horaActual = Hora()
>>> horaActual.horas = 9
>>> horaActual.minutos = 14
>>> horaActual.segundos = 30
>>> impriemHora(horaActual)

Para convertir imprimeHora en un método, todo lo que necesitamos hacer es mover la definición de la función al interior de la definición de la clase. Fíjese en cómo cambia el sangrado.

class Hora:
  def imprimeHora(hora):
    print str(hora.horas) + ":" +
          str(hora.minutos) + ":" +
          str(hora.segundos)

Ahora podemos invocar imprimeHora usando la notación de punto.

>>> horaActual.imprimeHora()

Como es habitual, el objeto sobre el que se invoca el método aparece delante del punto y el nombre del método aparece tras el punto.

El objeto sobre el que se invoca el método se asigna al primer parámetro, así que en este caso horaActual se asigna al parámetro hora.

Por convenio, el primer parámetro de un método se llama self. La razón de esto es un tanto rebuscada, pero se basa en una metáfora útil.

La sintaxis para la llamada a una función, imprimeHora(horaActual), sugiere que la función es el agente activo. Dice algo como "¡Oye imprimeHora! Aquí hay un objeto para que lo imprimas".

En programación orientada a objetos, los objetos son los agentes activos. Una invocación como horaActual.imprimeHora()dice "¡Oye horaActual! ¡Imprímete!"

Este cambio de perspectiva puede ser más elegante, pero no es obvio que sea útil. En los ejemplos que hemos visto hasta ahora, puede no serlo. Pero a veces transferir la responsabilidad de las funciones a los objetos hace posible escribir funciones más versátiles, y hace más fácil mantener y reutilizar código.

14.3 Otro ejemplo

Vamos a convertir incremento (de la Sección 13.3) en un método. Para ahorrar espacio, dejaremos a un lado los métodos ya definidos, pero usted debería mantenerlos en su versión:

class Hora:
  #aquí van las definiciones anteriores de métodos...

  def incremento(self, segundos):
    self.segundos = segundos + self.segundos

    while self.segundos >= 60:
      self.segundos = self.segundos - 60
      self.minutos = self.minutos + 1

    while self.minutos >= 60:
      self.minutos = self.minutos - 60
      self.horas = self.horas + 1

La transformación es puramente mecánica; hemos llevado la definición del método al interior de la definición de la clase y hemos cambiado el nombre del primer parámetro.

Ahora podemos invocar incremento como un método.

horaActual.incremento(500)

De nuevo, el objeto sobre el que invocamos el método se asigna al primer parámetro, self. El segundo parámetro, segundos toma el valor de 500.

Como ejercicio, convierta convertirASegundos(de la Sección 13.5) en un método de la clase Hora.

14.4 Un ejemplo más complicado

La función despues es ligeramente más complicada porque opera sobre dos objetos Hora, no sólo sobre uno. Sólo podemos convertir uno de los parámetros en self; el otro se queda como está:

class Hora:
  #aquí van las definiciones anteriores de métodos...

  def despues(self, hora2):
    if self.horas > hora2.horas:
      return 1
    if self.horas < hora2.horas:
      return 0

    if self.minutos > hora2.minutos:
      return 1
    if self.minutos < hora2.minutos:
      return 0

    if self.segundos > hora2.segundos:
      return 1
    return 0

Invocamos este método sobre un objeto y pasamos el otro como argumento:

if horaHecho.despues(horaActual):
  print "El pan estará hecho después de empezar."

Casi puede leer la invocación como una mezcla de inglés y español: "Si la hora-hecho es depués de la hora-actual, entonces..."

14.5 Argumentos opcionales

Hemos visto funciones internas que toman un número variable de argumentos. Por ejemplo, string.find puede tomar dos, tres o cuatro argumentos.

Es posible escribir funciones definidas por el usuario con listas de argumentos opcionales. Por ejemplo, podemos modernizar nuestra propia versión de encuentra para que haga lo mismo que string.find.

Esta es la versión original de la Sección 7.7:

def encuentra(cad, c):
  indice = 0
  while indice < len(cad):
    if str[indice] == c:
      return indice
    indice = indice + 1
  return -1

Esta es la versión aumentada y mejorada:

def encuentra(cad, c, comienzo=0):
  indice = comienzo
  while indice < len(cad):
    if str[indice] == c:
      return indice
    indice = indice + 1
  return -1

El tercer parámetro, comienzo, es opcional porque se proporciona un valor por omisión, 0. Si invocamos encuentra sólo con dos argumentos, utilizamos el valor por omisión y comenzamos por el principio de la cadena:

>>> encuentra("arriba", "r")
1

Si le damos un tercer parámetro, anula el predefinido:

>>> encuentra("arriba", "r", 2)
2
>>> encuentra("arriba", "r", 3)
-1

Como ejercicio, añada un cuarto parámetro, fin, que especifique dónde dejar de buscar.

Cuidado: Este ejercicio tiene truco. El valor por omisión de fin debería ser len(cad), pero eso no funciona. Los valores por omisión se evalúan al definir la función, no al llamarla. Cuando se define encuentra, cad aún no existe, así que no puede averiguar su longitud.

14.6 El método de inicialización

El método de inicialización es un método especial que se invoca al crear un objeto. El nombre de este método es __init__ (dos guiones bajos, seguidos de init y dos guiones bajos más). Un método de inicialización para la clase Hora es así:

class Hora:
  def __init__(self, horas=0, minutos=0, segundos=0):
    self.horas = horas
    self.minutos = minutos
    self.segundos = segundos

No hay conflicto entre el atributo self.horas y el parámetro horas. la notación de punto especifica a qué variable nos referimos.

Cuando invocamos el constructor Hora, los argumentos que damos se pasan a init:

>>> horaActual = Hora(9, 14, 30)
>>> horaActual.imprimeHora()
>>> 9:14:30

Como los parámetros son opcionales, podemos omitirlos:

>>> horaActual = Hora()
>>> horaActual.imprimeHora()
>>> 0:0:0

O dar sólo el primer parámetro:

>>> horaActual = Hora (9)
>>> horaActual.imprimeHora()
>>> 9:0:0

O los dos primeros parámetros:

>>> horaActual = Hora (9, 14)
>>> horaActual.imprimeHora()
>>> 9:14:0

Finalmente, podemos dar un subconjunto de los parámetros nombrándolos explicitamente:

>>> horaActual = Hora(segundos = 30, horas = 9)
>>> horaActual.imprimeHora()
>>> 9:0:30

14.7 Revisión de los Puntos

Vamos a reescribir la clase Punto de la Sección 12.1 con un estilo más orientado a objetos:

class Punto:
  def __init__(self, x=0, y=0):
    self.x = x
    self.y = y

  def __str__(self):
    return '(' + str(self.x) + ', ' + str(self.y) + ')'

El método de inicialización toma los valores de x e y como parámetros opcionales; el valor por omisión de cada parámetro es 0.

El siguiente método, __str__, devuelve una representación en forma de cadena de un objeto Punto. Si una clase ofrece un método llamado __str__, se impone al comportamiento por defecto de la función interna str de Python.

>>> p = Punto(3, 4)
>>> str(p)
'(3, 4)'

Imprimir un objeto Punto invoca implícitamente a __str__ sobre el objeto, así que definir __str__ también cambia el comportamiento de print:

>>> p = Punto(3, 4)
>>> print p
(3, 4)

Cuando escribimos una nueva clase, casi siempre empezamos escribiendo __init__, que facilita el instanciar objetos, y __str__, que casi siempre es útil para la depuración.

14.8 Sobrecarga de operadores

Algunos lenguajes hacen posible cambiar la definición de los operadores internos cuando se aplican a tipos definidos por el usuario. Esta característica se llama sobrecarga de operadores. Es especialmente útil cuando definimos nuevos tipos matemáticos.

Por ejemplo, para suplantar al operador de suma + necesitamos proporcionar un método llamado __add__:

class Punto:
  # aquí van los métodos que ya habíamos definido...

  def __add__(self, otro):
    return Punto(self.x + otro.x, self.y + otro.y)

Como es habitual, el primer parámetro es el objeto sobre el que se invoca el método. El segundo parámetro se llama convenientemente otro para distinguirlo del mismo (self). Para sumar dos Puntos, creamos y devolvemos un nuevo Punto que contiene la suma de las coordenadas x y la suma de las coordenadas y.

Ahora, cuando apliquemos el operador + a objetos Punto, Python invocará a __add__:

>>>   p1 = Punto(3, 4)
>>>   p2 = Punto(5, 7)
>>>   p3 = p1 + p2
>>>   print p3
(8, 11)

La expresión p1 + p2 equivale a p1.__add__(p2), pero es obviamente más elegante.

Como ejercicio, añada un método __sub__(self, otro) que sobrecargue el operador resta y pruébelo.

Hay varias formas de sobrecargar el comportamiento del operador multiplicación: definiendo un método llamado __mul__, o __rmul__, o ambos.

Si el operando a la izquierda de * es un Punto, Python invoca a __mul__, lo que presupone que el otro operando es también un Punto. Calcula el producto interno de dos puntos, definido según las reglas del álgebra lineal:

def __mul__(self, otro):
  return self.x * otro.x + self.y * otro.y

Si el operando a la izquierda de * es un tipo primitivo y el operando de la derecha es un Punto, Python invca a __rmul__, lo que realiza una multiplicación escalar:

def __rmul__(self, otro):
  return Punto(otro * self.x,  otro * self.y)

El resultado es un nuevo Punto cuyas coordenadas son múltiplos de las coordenadas originales. Si otro es un tipo que no se puede multiplicar por un número en coma flotante, entonces __rmul__ causará un error.

Este ejemplo muestra ambos tipos de multiplicación:

>>> p1 = Punto(3, 4)
>>> p2 = Punto(5, 7)
>>> print p1 * p2
43
>>> print 2 * p2
(10, 14)

¿Qué ocurre si intentamos evaluar p2 * 2? Como el primer parámetro es un Punto, Python invoca a __mul__ con 2 como el segundo parámetro. Dentro de __mul__, el programa intenta acceder a la coordenada x de otro, pero no lo consigue porque un entero no tiene atributos:

>>> print p2 * 2
AttributeError: 'int' object has no attribute 'x'

Desgraciadamente, el mensaje de error es un poco opaco. Este ejemplo muestra algunas de las difucultades de la programación orientada a objetos. A veces es difícil averiguar simplemente qué código se está ejecutando.

Para ver un ejemplo más completo de sobrecarga de operadores, vaya al Apéndice B.

14.9 Polimorfismo

La mayoría de los métodos que hemos escrito funcionan sólo para un tipo específico. Cuando usted crea un nuevo objeto, escribe métodos que operan sobre ese tipo.

Pero hay ciertas operaciones que querrá aplicar a muchos tipos, como las operaciones aritméticas de las secciones anteriores. Si muchos tipos admiten el mismo conjunto de operaciones, puede escribir funciones que trabajen sobre cualquiera de esos tipos.

Por ejemplo, la operación multisuma (común en álgebra lineal) toma tres parámetros; multiplica los dos primeros y luego suma el tercero. Podemos escribirla en Python así:

def multisuma (x, y, z):
  return x * y + z

Este método trabajará con cualquier valor de x e yque se pueda multiplicar y con cualquier valor de z que se pueda sumar al producto.

Podemos invocarlo con valores numéricos:

>>> multisuma (3, 2, 1)
7

O con Puntos:

>>> p1 = Punto(3, 4)
>>> p2 = Punto(5, 7)
>>> print multisuma (2, p1, p2)
(11, 15)
>>> print multisuma (p1, p2, 1)
44

En el primer caso, el Punto se multiplica por un escalar y luego se suma a otro Punto. En el segundo caso, el producto interior produce un valor numérico, así que el tercer parámetro también debe ser un valor numérico.

Una función como ésta que puede tomar parámetros con diferentes tipos se llama polimórfica.

Como un ejemplo más, observe el método delDerechoYDelReves, que imprime dos veces una lista, hacia adelante y hacia atrás:

def delDerechoYDelReves(derecho):
  import copy
  reves = copy.copy(derecho)
  reves.reverse()
  print str(derecho) + str(reves)

Como el método reverse es un modificador, hacemos una copia de la lista antes de darle la vuelta. Así, este método no modifica la lista que recibe como parámetro.

He aquí un ejemplo que aplica delDerechoYDelReves a una lista:

>>>   miLista = [1, 2, 3, 4]
>>>   delDerechoYDelReves(miLista)
[1, 2, 3, 4][4, 3, 2, 1]

Por supuesto, pretendíamos aplicar esta función a listas, así que no es sorprendente que funcione. Lo sorprendente es que pudiéramos usarla con un Punto.

Para determinar si una función se puede aplicar a un nuevo tipo, aplicamos la regla fundamental del polimorfismo:

Si todas las operaciones realizadas dentro de la función se pueden aplicar al tipo, la función se puede aplicar al tipo.

Las operaciones del método incluyen copy, reverse y print.

copy trabaja sobre cualquier objeto, y ya hemos escrito un método __str__ para los Puntos, así que todo lo que necesitamos es un método reverse en la clase Punto:

def reverse(self):
  self.x , self.y = self.y, self.x

Ahora podemos pasar Puntos a delDerechoYDelReves:

>>>   p = Punto(3, 4)
>>>   delDerechoYDelReves(p)
(3, 4)(4, 3)

El mejor tipo de polimorfismo es el que no se busca, cuando usted descubre que una función que había escrito se puede aplicar a un tipo para el que nunca la había planeado.

14.10 Glosario

lenguaje orientado a objetos
Un lenguaje que ofrece características, como clases definidas por el usuario y herencia, que facilitan la programación orientada a objetos.
programación orientada a objetos
Un estilo de programación en el que los datos y las operaciones que los manipulan están organizadas en clases y métodos.
método
Una función definida dentro de una definición de clase y que se invoca sobre instancias de esa clase.
imponer
Reemplazar una opción por omisión. Los ejemplos incluyen el reemplazo de un parámetro por omisión con un argumento particular y el reemplazo de un método por omisión proporcionando un nuevo método con el mismo nombre.
método de inicialización
Un método especial que se invoca automáticamente al crear un nuevo objeto y que inicializa los atributos del objeto.
sobrecarga de operadores
Ampliar los operadores internos (+, -, *, >, <, etc.) de modo que trabajen con tipos definidos por el usuario.
producto interno
Una operación definida en álgebra lineal que multiplica dos Puntos y entrega un valor numérico.
multiplicación escalar
Una operación definida en álgebra lineal que multiplica cada una de las coordenadas de un Punto por un valor numérico.
polimórfica
Una función que puede operar sobra más de un tipo. Si todas las operaciones realizadas dentro de una función se pueden aplicar a un tipo, la función se puede aplicar a ese tipo.


Next Up Previous Hi Index