Sobrecarga de operadores en Python
Publicado el

Sobrecarga de operadores en Python

Escrito por Alejandro Sánchez Yalí. Publicado originalmente el 2021-06-15 en el blog de Monadical.

Python es un lenguaje de programación orientado a objetos y una de sus características es que soporta la sobrecarga de operadores, es decir, nos permite redefinir el comportamiento de los operadores nativos (+,-, *, /, +=, -=, **, etc.). Esto significa que podemos crear código con mayor legibilidad, ya que usamos operadores nativos para definir nuevas representaciones, comparaciones y operaciones entre objetos que hemos creado.

Para ilustrar cómo funciona la sobrecarga de operadores, te guiaré a través de cómo redefinir el comportamiento de los operadores + y - utilizando los métodos especiales add y sub de las clases de Python.

Usando la sobrecarga de operadores

La mejor manera de entender una idea como esta es verla en práctica. Así que, comencemos con un ejercicio que hace necesario redefinir el comportamiento de los operadores + y - de Python.

Digamos que tenemos un reloj de 24 horas, y necesitamos saber qué hora mostrará el reloj dentro de 10 horas. Por ejemplo, si ahora son las 18:00 de la tarde, 10 horas más tarde el reloj indicará que son las 4:00 de la mañana–18:00 horas + 10:00 horas = 4:00 horas. Así, la suma del tiempo en un reloj de 24 horas no es como la suma habitual de números naturales, enteros o reales.

Relojes - Operación Aritmética Modular 24

Figura 1. Relojes - Operación Aritmética Modular 24.

El objetivo de este ejercicio es entender cómo se puede redefinir el comportamiento de los operadores de suma y resta (+, -) para capturar adecuadamente la aritmética del reloj, de modo que se pueda sumar y restar el "tiempo del reloj" (en horas) para dar resultados apropiados. Empecemos.

Inicialmente, vamos a tener una clase llamada Clock en la que representaremos el tiempo con el formato HH:MM:

class Clock:

   def __init__(self, time: str):
       self.hour, self.min = [int(i) for i in time.split(':')]

   def __repr__(self) -> str:
       min = '0' + str(self.min)
       return str(self.hour) + ':' + min[-2:]

Ten en cuenta que esperamos que el usuario ingrese la variable time como una cadena en el formato HH:MM. También hemos hecho uso del método __repr__ para definir la representación en consola de nuestra clase, nuevamente en el formato HH:MM. Vamos a instanciarla y ejecutarla:

time_1 = Clock('10:30')
time_1

La salida de la consola será:

10:30

Ahora podemos crear dos instancias de la clase Clock:

time_1 = Clock('10:30')
time_2 = Clock('19:45')

Si intentamos en este punto sumar estas instancias, encontramos el siguiente error:

time_1 + time_2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-17-9693c7986cbd> in <module>()
----> 1 time_1 + time_2

TypeError: unsupported operand type(s) for +: 'Clock' and 'Clock'

El problema aquí es que el operador + no entiende los operandos de la clase Clock.

Podemos corregir este error añadiendo un método asociado a la suma a la clase Clock. En Python, este método se llama __add__ y requiere dos parámetros. El primero, self, siempre es requerido, y el segundo, other, representa otra instancia de la misma clase. Por ejemplo, a.__add__(b) le pedirá al objeto Clock a que se sume al objeto Clock b. Esto se puede escribir en la notación estándar, a + b. Para nuestro caso, la suma se puede definir de la siguiente manera:

def __add__(self, other: Clock) -> Clock:
       hour, min = divmod(self.min + other.min, 60)
       hour = (hour + self.hour + other.hour) % 24
       return self.__class__(str(hour) + ':' + str(min))

Observa cómo se usa divmod aquí. Esta función realiza una división–la misma operación que hacemos con el operador de división (/)–pero devuelve dos valores: el cociente y el residuo. divmod convierte el número total de minutos al formato HH:MM. El número de minutos se divide por 60 de modo que el cociente representa las horas y el residuo representa los minutos. Como el reloj usa los dígitos 0, 1, 2 … 24 para representar las horas, calculamos el número total de horas módulo 24.

Finalmente, al final de la expresión, se usa self.__class__(str(hour) + ':' + str(min)) para crear una nueva instancia de la clase Clock de modo que el resultado pueda ser reutilizado en cálculos posteriores.

Hagamos dos instancias de la clase Clock:

time_1 = Clock('10:30')
time_2 = Clock('19:45')

Si las sumamos, obtenemos:

time_1 + time_2
6:15

Este es exactamente el resultado que queremos. De manera similar, podemos redefinir el comportamiento del operador (-) usando el método __sub__:

def __sub__(self, other: Clock) -> Clock:
        hour, min = divmod(self.min - other.min, 60)
        hour = (hour + self.hour - other.hour) % 24
        return self.__class__(str(hour) + ':' + str(min))

Podemos hacerlo de tal manera que la clase final será:

Class Clock:

   def __init__(self, time):
       self.hour, self.min = [int(i) for i in time.split(':')]

   def __repr__(self) -> str:
       min = '0' + str(self.min)
       return str(self.hour) + ':' + min[-2:]

   def __add__(self, other: Clock) -> Clock:
       hour, min = divmod(self.min + other.min, 60)
       hour = (hour + self.hour + other.hour) % 24
       return self.__class__(str(hour) + ':' + str(min))

   def __sub__(self, other: Clock) -> Clock:
       hour, min = divmod(self.min - other.min, 60)
       hour = (hour + self.hour - other.hour) % 24
       return self.__class__(str(hour) + ':' + str(min))

Ahora es posible operar directamente sobre objetos Clock usando los operadores + y -, en lugar de llamar a métodos:

time_1 = Clock('10:30')
time_2 = Clock('19:45')
time_3 = Clock('16:16')

time_1 - time_2 + time_3
7:01

Métodos para sobrecargar operadores

Como vimos en la sección anterior, la sobrecarga de operadores1 nos permite redefinir el comportamiento de los operadores aritméticos (+, -) y de hecho, se puede hacer con cualquiera de los operadores aritméticos, binarios, de comparación y lógicos de Python. Podemos usar los siguientes métodos especiales para redefinir cualquiera de los operadores:

OperaciónSintaxisFunción
Adicióna + badd(a, b)
Concatenaciónseq1 + seq2concat(seq1, seq2)
Prueba de contenciónobj in seqcontains(seq, obj)
Divisióna / btruediv(a, b)
Divisióna // bfloordiv(a, b)
Divisióndivmod(a, b)divmod(a, b)
AND a nivel de bitsa & band_(a, b)
XOR a nivel de bitsa ^ bxor(a, b)
Inversión a nivel de bits~ ainvert(a)
OR a nivel de bitsa | bor_(a, b)
Exponenciacióna ** bpow(a, b)
Identidada is bis_(a, b)
Identidada is not bis_not(a, b)
Asignación indexadaobj[k] = vsetitem(obj, k, v)
Eliminación indexadadel obj[k]delitem(obj, k)
Indexaciónobj[k]getitem(obj, k)
Desplazamiento a la izquierdaa << blshift(a, b)
Móduloa % bmod(a, b)
Multiplicacióna * bmul(a, b)
Negación (Aritmética)- aneg(a)
Negación (Lógica)not anot_(a)
Positivo+ apos(a)
Desplazamiento a la derechaa >> brshift(a, b)
Asignación de rebanadaseq[i:j] = valuessetitem(seq, slice(i, j), values)
Eliminación de rebanadadel seq[i:j]delitem(seq, slice(i, j))
Rebanadoseq[i:j]getitem(seq, slice(i, j))
Formateo de cadenass % objmod(s, obj)
Sustraccióna - bsub(a, b)
Prueba de verdadobjtruth(obj)
Ordenamientoa < blt(a, b)
Ordenamientoa <= ble(a, b)
Igualdada == beq(a, b)
Diferenciaa != bne(a, b)
Ordenamientoa >= bge(a, b)
Ordenamientoa > bgt(a, b)

Cada objeto tiene varios métodos especializados que se utilizan para interactuar con otros objetos o con operadores nativos de Python. Al igual que con el ejemplo de la aritmética del reloj, cada uno de estos métodos puede ser implementado de acuerdo con el siguiente esquema de implementación:

def __«operador»__(self, other: Object) -> Object:
       «instrucciones»
       return «salida»

Aquí necesitamos seleccionar el «operador» y definir las «instrucciones» internas y la «salida» para personalizar su comportamiento.

La sobrecarga de operadores nos permite definir nuevas estructuras matemáticas, como grupos cíclicos, campos finitos, espacios vectoriales, grupos, anillos y módulos. Hay aplicaciones útiles para esto en criptografía, matemáticas discretas y cálculo avanzado. Echa un vistazo a la documentación de Python para aprender más sobre este tema y cómo se pueden sobrecargar los otros operadores.

Finalmente, si hay algún error, omisión o inexactitud en este artículo, no dudes en contactarnos a través del siguiente canal de Discord: Math & Code.

Referencias

Footnotes

  1. No todos los lenguajes soportan la sobrecarga de operadores. Aunque la sobrecarga de operadores puede ser más conveniente y permitir un código más elegante