Objetivo: Introducción a la sobrecarga de operadores en Python.
Consideremos una operación aritmética primordial, la primera con la que nos atrevimos a coquetear, siendo unos críos, una vez aprendimos a utilizar los números para contar los objetos de nuestro mundo alrededor: la suma.
Todos sabemos sumar. Python, por supuesto, también:
>>> 2 + 3
5
Tal vez nunca te hayas parado a pensar que Python no sólo sabe sumar números:
>>> 'caperucita' + 'roja'
'caperucitaroja'
Pues sí, Python también sabe «sumar» cadenas de caracteres: es lo que se denomina concatenación.
Observa, simplemente como nota, que en este último ejemplo la operación suma no cumple la propiedad conmutativa: no es lo mismo sumar ‘caperucita’ + ‘roja’, que ‘roja’ + ‘caperucita’.
Python también sabe que hay cosas que no puede sumar. Si tratamos de engañarle con código como éste, se queja con razón:
>>> 'caperucita' + 5
Traceback (most recent call last):
File "<pyshell#46>", line 1, in <module>
'caperucita' + 5
TypeError: Can't convert 'int' object to str implicitly
En efecto, Python no sabe qué hacer cuando le dices que sume un número a una cadena de caracteres. Si lo que pretendíamos era concatenar ambos era preciso convertir previamente el número a string.
La suma también aparece definida para otros tipo de objetos, como por ejemplo las listas:
>>> [1, 2] + [3, 4]
[1, 2, 3, 4]
Sin embargo, Python no admite la suma de conjuntos:
>>> {1, 2} + {3, 4}
Traceback (most recent call last):
File "<pyshell#48>", line 1, in <module>
{1, 2} + {3, 4}
TypeError: unsupported operand type(s) for +: 'set' and 'set'
Donde quiero ir a parar es que te fijes que, para un mismo operador, el ‘+’, Python sabe cómo actuar dependiendo del tipo de objetos, los operandos, que coloquemos a la izquierda y a la derecha del operador.
Así pues, si los operandos son números, efectuará una suma aritmética; si son cadenas de caracteres las concatenará y otro tanto de lo mismo si son listas. Además, Python sabe qué «sumas» no están permitidas.
Se podía haber elegido un operador diferente a ‘+’ para la concatenación de strings, pero no sería muy adecuado, pues intuitivamente tiene pleno significado para nosotros el uso del ‘+’ para una concatenación. Sería mucho más incómodo tener que recordar un operador distinto. Y tanto más cuántos más tipos de objetos diferentes quisiéramos sumar.
Llegamos al quid de la cuestión: un mismo operador, pero muchas maneras de comportarse en función del tipo de operandos que empleemos.
A esta exquisita filosofía de diseño se le denomina sobrecarga de operadores, reflejando el hecho de que un mismo operador (en el ejemplo de hoy, la suma), «carga sobre sus espaldas» código para saber cómo responder ante operandos de distinta naturaleza.
La sobrecarga de operadores, unida a la sobrecarga de métodos, tema que abordaremos en otra ocasión, está relacionada con una de las características más refinadas de la POO y uno de sus pilares: el polimorfismo, ser capaz de comportarse de múltiples formas diferentes presentando una misma interfaz común al programador.
¿Podríamos sobrecargar aún más nuestro ya de por sí sobrecargado operador suma? Planteando la cuestión de otro modo: ¿podríamos implementar una suma de objetos pertenecientes a clases creadas por nosotros?
La respuesta es afirmativa y aprenderemos a hacerlo a continuación.
Comencemos por un ejemplo sencillo. Imaginemos que creamos una clase que representa a vectores libres en el plano caracterizados por sus dos componentes x e y. Sería intuitivamente práctico poder definir una operación suma de dos vectores.
Por ejemplo, si tenemos un vector v1 = (1, 1) y otro v2 = (5, -3), queremos definir la suma de vectores de tal modo que v1 + v2 sea el esperado (6, -2).
Esta sería nuestra eventual clase Vector:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
No me he complicado mucho y únicamente he incluido el inicializador, que se ocupa de crear las variables miembro x e y referenciando a los valores facilitados durante la creación del objeto.
Para que esta clase sobrecargue el operador ‘+’ y podamos hablar de suma de dos vectores necesitamos incluir en la clase un método especial:__add__. Al igual que __init__, observa que comienza y termina con dos guiones bajos, indicando que es un nombre con un significado especial para Python.
El código que incluyamos en ese método «mágico» especial determinará cómo se comportará la clase ante la operación suma.
Es importante tener muy claro algo antes. La suma objeto1 + objeto2 es equivalente a aplicar el método __add__ sobre objeto1, recibiendo como argumento objeto2. Es decir:
objeto1.__add__(objeto2)
Asegúrate de entender bien esto, pues es todo el misterio que tiene la sobrecarga de operadores: objeto1, el operando de la izquierda del +, debe tener un método __add__ que recibirá como argumento el operando de la derecha, objeto2. Esto se manifestará crucial en el ejemplo que pondremos un poco más adelante.
La clase Vector modificada sobrecargando la suma queda así:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, v):
sumax = self.x + v.x
sumay = self.y + v.y
vectorsuma = Vector(sumax, sumay)
return vectorsuma
Analicemos con cuidado el código del método __add__:
Aparte de self, presente en todos los métodos como primer parámetro, y que referenciará al objeto actual cuando se cree (el de la izquierda del +), observa que toma otro parámetro, que hemos denominado v, que no es otra cosa sino el vector a la derecha del +. En el código de la función distinguirás como, por un lado, nos referimos a las componentes x e y del objeto propio como self.x y self.y, mientras que las del segundo operando, facilitado como argumento, son v.x y v.y.
El código suma las x con las x y las y con las y:
sumax = self.x + v.x
sumay = self.y + v.y
A continuación, invoca al constructor de la clase Vector para crear un nuevo vector, devuelto como vectorsuma, de componentes con el resultado de estas sumas:
vectorsuma = Vector(sumax, sumay)
return vectorsuma
Veamos, ahora sí, la clase al completo y un ejemplo de su utilización:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, v):
sumax = self.x + v.x
sumay = self.y + v.y
vectorsuma = Vector(sumax, sumay)
return vectorsuma
>>> v1 = Vector(1, 1)
>>> v2 = Vector (5, -3)
>>> vectorsuma = v1 + v2
>>> vectorsuma.x
6
>>> vectorsuma.y
-2
El ejemplo que figura a continuación ilustra la aplicación de la sobrecarga del operador suma en el caso de objetos de diferentes clases.
Consideremos, por ejemplo, una clase denominada Camiseta que mantiene información sobre el color, talla y tipo de manga de ciertas camisetas deportivas. Por otro lado, la clase Pantalon registra el color y talla de pantalones de deporte.
Podríamos imaginarnos algo así:
camiseta + pantalón = un equipamiento deportivo completo
Es decir, aparecería en escena una tercera clase, Equipo, que tendría como constituyentes un objeto del tipo Camiseta y otro del tipo Pantalon.
Estas son nuestras tres clases:
class Camiseta:
def __init__(self, color, talla, manga):
self.color = color
self.talla = talla
self.manga = manga
class Pantalon:
def __init__(self, color, talla):
self.color = color
self.talla = talla
class Equipo:
def __init__(self, camiseta, pantalon):
self.camiseta = camiseta
self.pantalon = pantalon
Y ahora la clave. Debemos dotar de sentido a la expresión camiseta + pantalon. Para ello, recuerda lo que dijimos más arriba:
camiseta + pantalon = camiseta.__add__(pantalon)
El objeto de la izquierda del más debe contar con un método especial, __add__ que recibirá como argumento el objeto de la derecha del +.
Por lo tanto, la clase Camiseta, que es la presente a la izquierda del +, precisa definir el método __add__. Veámoslo:
def __add__(self, pantalon):
return Equipo(self, pantalon)
Simplemente invoca al constructor de la clase Equipo para construir un nuevo objeto con la camiseta (self)y pantalón facilitados.
Veamos las clases al completo junto a un ejemplo de utilización:
class Camiseta:
def __init__(self, color, talla, manga):
self.color = color
self.talla = talla
self.manga = manga
def __add__(self, pantalon):
return Equipo(self, pantalon)
class Pantalon:
def __init__(self, color, talla):
self.color = color
self.talla = talla
class Equipo:
def __init__(self, camiseta, pantalon):
self.camiseta = camiseta
self.pantalon = pantalon
>>> camiseta1 = Camiseta('blanca', 'L', 'corta')
>>> pantalon1 = Pantalon('azul', 'XL')
>>> equipo1 = camiseta1 + pantalon1
>>> equipo1.camiseta.color
'blanca'
>>> equipo1.pantalon.talla
'XL'
Observa la doble notación punto para acceder a los miembros de un objeto que están, a su vez, dentro de otro objeto.
Fíjate muy bien que sólo hemos definido el método __add__ en la clase Camiseta. Por lo tanto, si invirtiéramos el orden de los sumandos, la aplicación no funcionaría. En efecto:
>>> equipo2 = pantalon1 + camiseta1
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
equipo2 = pantalon1 + camiseta1
TypeError: unsupported operand type(s) for +: 'Pantalon' and 'Camiseta'
Esto es así porque pantalon1 + camiseta1 es equivalente a:
pantalon1.__add__(camiseta1)
Y la clase Pantalon no tiene definido ningún método __add__.
Si, además de sumar camisetas más pantalones, deseas sumar pantalones más camisetas, necesitas definir también un método __add__en la clase Pantalon (te lo dejo como ejercicio). No presupongas nunca que se cumple la propiedad conmutativa salvo que expresamente la definas.
Existen muchos más operadores que se pueden sobrecargar, pero la mecánica es similar a la aplicada aquí, salvo con otros métodos «mágicos» diferentes. Los veremos en otros artículos. Pero mi consejo es que no esperes y los busques por ti mismo, no te será difícil encontrarlos. Si has entendido bien este artículo no deberá suponerte el mínimo esfuerzo comprender su funcionamiento.
Javier Montero Gabarró
El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)
El Club del Autodidacta
Consulta el índice completo de artículos relacionados con Python.