Curso
Uno de los factores que hacen de Python un lenguaje de programación tan potente son los métodos mágicos. En algunos círculos de Python escucharás que los llaman "métodos dunder" por su construcción con doble guion bajo.
Su función en Python es llevar a cabo lo que se conoce como "sobrecarga de operadores", que no es más que dar un significado ampliado a un operador predefinido. En otras palabras, la sobrecarga de operadores sirve para añadir comportamientos personalizados a nuestras clases y poder usarlas con los operadores y la funcionalidad integrada de Python.
En este artículo, profundizaremos en los métodos mágicos de Python:
- Diferenciando métodos y funciones
- Explicando qué son los métodos mágicos
- Mostrando ejemplos en Python.
Echa un vistazo a este cuaderno de DataLab para seguir el código utilizado en este tutorial.
Métodos en Python vs. funciones
Antes de entrar en qué son los métodos mágicos, conviene entender la diferencia entre un método y una función en Python.
Python utiliza los métodos para representar el comportamiento de un objeto. Por tanto, los métodos están asociados a un objeto y realizan operaciones sobre los datos de ese objeto. Otra característica clave de un método es el parámetro self, que siempre debe aparecer como el primer parámetro que se le pasa.
Nota: no tiene por qué llamarse self como primer parámetro, pero la comunidad de Pythonistas coincide en que es una buena práctica.
# Example of a method
class Human:
# This is a method
def walk(self):
print("I am walking")
Los métodos dependen de la clase con la que están asociados. Por tanto, para llamar a un método, primero hay que crear una instancia de la clase y después acceder al método con el operador punto.
# This will work
joe = Human()
joe.walk()
"""
I am walking
"""
# This will throw a NameError
walk()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
/tmp/ipykernel_153/2327344440.py in <module>
4
5 # This will throw a NameError
----> 6 walk()
NameError: name 'walk' is not defined
Una función, en cambio, es diferente: no tenemos que crear una instancia de una clase.
Las funciones en Python son pasos que se ejecutan como un bloque de código solo cuando se llaman. Se definen de forma independiente de cualquier objeto de clase, por lo que se pueden invocar directamente por su nombre. Como no están asociadas a ninguna clase, no hay que pasar self como parámetro; los parámetros son opcionales.
En esencia, una función es simplemente un conjunto de instrucciones bien definidas que el intérprete utiliza para realizar una tarea.
# Example of a function
def run():
print("I am running.")
run()
"""
I am running.
"""
Como explicamos en nuestro artículo Cómo llamar y escribir funciones, las funciones también pueden ser de dos tipos: integradas y definidas por el usuario.
- Funciones integradas: funciones incluidas en la biblioteca estándar de Python.
- Funciones definidas por el usuario: funciones definidas por ti; Python permite definir y usar funciones propias a las que podrás acceder desde un programa.
En muchos aspectos, métodos y funciones son idénticos (ambos se definen con la sentencia def). La diferencia de fondo es que los métodos están asociados a un objeto y se usan para operar sobre los datos contenidos en una clase, mientras que las funciones se definen de forma independiente y pueden llamarse por su nombre en cualquier parte del programa.
¿Qué son los métodos mágicos de Python?
Al principio del tutorial mencionamos que los métodos mágicos también se llaman métodos dunder por su doble guion bajo. Con esto queremos decir que todos los métodos mágicos de Python empiezan y terminan con dos guiones bajos.
El método mágico más conocido es __init__, que permite inicializar una clase con atributos concretos.
class Employee:
def __init__(self, name, age, role):
self.name = name
self.age = age
self.role = role
Para instanciar esta clase, debemos especificar los atributos name, age y role de la instancia. Por ejemplo:
user_1 = Employee(name="John", age=24, role="Data Scientist")
Si no lo hacemos, se lanzará un TypeError.
El método __init__ pertenece a la categoría de métodos mágicos de inicialización y construcción, pero es solo uno de muchos. La siguiente tabla recoge algunos otros métodos mágicos de inicialización y construcción.
|
__new__(cls, other) |
Se usa para crear una instancia nueva de una clase |
|
__init__(self, other) |
Se llama después de crear una instancia nueva; todos los atributos pasados al constructor de la clase se utilizarán como atributos. |
|
__del__(self) |
También conocido como destructor; se llama cuando una instancia va a destruirse. |
La forma más sencilla de entender los métodos mágicos es como un contrato entre tu implementación y el intérprete de Python. Una de las cláusulas del contrato exige que Python realice ciertas acciones bajo el capó ante determinadas circunstancias, como inicializar una instancia de clase con objetos concretos.
Dicho de otro modo: no es tu responsabilidad invocar directamente un método mágico; Python lo hace por ti entre bambalinas.
Métodos de representación en forma de cadena
Python es un lenguaje de programación orientada a objetos (POO); en Python, todo es un objeto. Al crear un nuevo objeto, implícitamente creamos un objeto relacionado porque todas las clases heredan de object.
Nota: también puedes hacer la herencia explícita pasando la clase object a tu clase.
# This works
class Car:
pass
# This also works
class Vehicle(object):
pass
car = Car()
vehicle = Vehicle()
print(car, vehicle, sep="\n")
"""
<__main__.Car object at 0x7f80449dc0a0>
<__main__.Vehicle object at 0x7f80449dc1f0>
"""
En el código anterior, creamos dos clases vacías, pero el intérprete de Python siguió sabiendo qué imprimir cuando llamamos a print. Esto es posible porque todas las clases heredan de la clase object (la madre de todas las clases en Python), y esta tiene un método mágico llamado __repr__, al que llama la sentencia print para devolver un valor a nuestro programa principal.
Quienes estén familiarizados con la herencia sabrán que una clase hija puede sobrescribir a su clase padre definiendo su propio método con el mismo nombre.
# Overriding the object __repr__
class Vehicle:
def __repr__(self):
return f"{self.__class__.__qualname__}"
vehicle = Vehicle()
print(vehicle)
"""
Vehicle
"""
También puedes usar el método mágico __str__ para crear una representación en cadena que se llame con print.
De hecho, el método mágico __repr__ actúa como respaldo del método mágico __str__. Así, cuando llamamos a print, Python busca primero el método __str__ definido en tu clase y, si no lo encuentra, recurre a __repr__.
# Using __str__ for string representation
class Vehicle:
def __str__(self):
return f"{self.__class__.__qualname__}"
vehicle = Vehicle()
print(vehicle)
"""
Vehicle
"""
La siguiente tabla recoge otros métodos mágicos de representación en forma de cadena.
|
__str__(self) |
Lo llama la función integrada str() de Python y la sentencia print para mostrar la representación informal de un objeto. Ten en cuenta que no tiene por qué ser una expresión válida de Python. |
|
__repr__(self) |
Lo llama la función integrada repr() de Python y las conversiones de cadena para mostrar la representación oficial de un objeto. Idealmente debería parecer una expresión válida de Python, pero si no es posible, se prefiere una cadena con una descripción útil. |
|
__unicode__(self) |
Se llama para implementar la función integrada unicode() de Python y devuelve un objeto Unicode. |
|
__hash__(self) |
Lo llama la función integrada hash() de Python y las operaciones sobre miembros de colecciones con hash. Este método debe devolver un entero. |
|
__nonzero__(self) |
Lo llama la operación integrada bool() de Python para evaluar la veracidad. El método debe devolver True o False (o sus equivalentes enteros 1 o 0). |
Métodos "mágicos" de encapsulación
La encapsulación es uno de los conceptos fundamentales de la programación orientada a objetos (POO). Se refiere a agrupar los datos con los métodos que operan sobre ellos o a restringir el acceso directo a ciertos componentes dentro de un objeto.
Desarrolladores que vienen de otros lenguajes POO suelen criticar que Python no adopta la encapsulación de clases de forma estricta. Están acostumbrados a modificadores explícitos para métodos o campos que permiten definir, por ejemplo, atributos privados con getters y setters públicos. La diferencia en Python es que gran parte de la encapsulación existente se consigue mediante métodos mágicos.
Por ejemplo, __setattr__ es una solución de encapsulación que nos permite definir el comportamiento de la asignación a un atributo, exista o no. Es decir, se pueden definir reglas para los cambios de valor de los atributos.
import string
class Alphabet():
def __setattr__(self, name, value):
self.__dict__[name] = value.upper()
a = Alphabet()
# Assinging an attribute that does not exist
# The setattr should convert it to caps despite being set as lowercase
a.alphabet = string.ascii_lowercase
print(a.alphabet)
En el código anterior creamos una clase llamada Alphabet sin atributos, pero le indicamos a Python que queremos poner en mayúsculas cualquier atributo que asignemos a la instancia de la clase.
Entendemos que la POO puede quedarte un poco lejos. Para ponerte al día con POO en Python, consulta el itinerario de aprendizaje Python Programmer de DataCamp.
Métodos mágicos matemáticos
Otra categoría de métodos mágicos en Python son los métodos matemáticos (también conocidos como operadores aritméticos). Las expresiones se crean usando símbolos especiales llamados "operadores" en Python. Usar estos operadores en una expresión implica crear objetos con métodos mágicos matemáticos. Si no se cumple esta regla, se lanzará un TypeError.
class Numbers:
def __init__(self, a, b):
self.a = a
self.b = b
set_a = Numbers(2, 4)
set_b = Numbers(3, 5)
print(set_a + set_b)
"""
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/tmp/ipykernel_152/3639634723.py in <module>
7 set_b = Numbers(3, 5)
8
----> 9 print(set_a + set_b)
TypeError: unsupported operand type(s) for +: 'Numbers' and 'Numbers'
"""
En el código anterior intentamos crear varias instancias de la clase Numbers y sumarlas, lo que, como era de esperar, genera un error. Una solución sería crear un método en la clase para sumar números, pero esto nos impediría crear expresiones.
Una solución mejor es implementar un método mágico __add__ como sigue:
# Using add operator
class Numbers:
def __init__(self, a, b):
self.a = a
self.b = b
def __add__(self, other):
# Only permit Numbers objects to be added
if not isinstance(other, Numbers):
return NotImplemented
return Numbers(other.a + self.a, other.b + self.b)
def __repr__(self):
return f"{self.__class__.__qualname__}({self.a}, {self.b})"
a = Numbers(2, 4)
b = Numbers(3, 5)
print(a + b) # Should result in Numbers(5, 9)
"""
RandomNumbers(5, 9)
"""
El ejemplo anterior solo nos permite sumar dos objetos Numbers. Vamos a implementar otro método mágico matemático que nos permita multiplicar un objeto Numbers por un entero.
# Multiplying Numbers with an integer
class Numbers:
def __init__(self, a, b):
self.a = a
self.b = b
def __add__(self, other):
# Only permit Numbers objects to be added
if not isinstance(other, Numbers):
return NotImplemented
return Numbers(other.a + self.a, other.b + self.b)
def __mul__(self, other):
if not isinstance(other, int):
return NotImplemented
return Numbers(self.a * other, self.b * other)
def __repr__(self):
return f"{self.__class__.__qualname__}({self.a}, {self.b})"
a = Numbers(2, 4)
print(a * 5) # Should return 10, 20
"""
RandomNumbers(10, 20)
"""
En el código anterior añadimos un método mágico __mul__ para poder multiplicar los números de nuestra clase personalizada por un entero.
Fíjate en que el objeto Numbers está a la izquierda del operador *. No es casualidad. Pongámoslo a la derecha del operando y veamos qué pasa.
- snip --
a = Numbers(2, 4)
print(3 * a)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/tmp/ipykernel_152/2857655049.py in <module>
1 a = Numbers(2, 4)
----> 2 print(3 * a)
TypeError: unsupported operand type(s) for *: 'int' and 'Numbers'
Python lanza un TypeError.
El método mágico __mul__ que implementamos solo contempla el caso en que el objeto Numbers está a la izquierda del operador *. Para ampliar esta funcionalidad, debemos añadir el método mágico inverso, __rmul__.
# Enabling reverse multiplication
class Numbers:
def __init__(self, a, b):
self.a = a
self.b = b
def __add__(self, other):
# Only permit Numbers objects to be added
if not isinstance(other, Numbers):
return NotImplemented
return Numbers(other.a + self.a, other.b + self.b)
def __mul__(self, other):
if not isinstance(other, int):
return NotImplemented
return Numbers(self.a * other, self.b * other)
def __rmul__(self, other):
return self.__mul__(other)
def __repr__(self):
return f"{self.__class__.__qualname__}({self.a}, {self.b})"
a = Numbers(2, 4)
print(5 * a) # Should return 10, 20
"""
Numbers(10, 20)
"""
Ahora podemos crear una expresión de multiplicación sin preocuparnos de en qué lado del operando aparece el objeto Numbers.
Cierre
El principio básico para aprovechar los métodos mágicos de Python es siempre el mismo: crea una clase y sobrescribe el método mágico de la clase object. Hay muchísimos métodos mágicos en Python, así que este tutorial sería larguísimo si quisiéramos cubrirlos todos uno a uno. Te recomendamos consultar la documentación de Python para hacerte una idea del alcance completo de los métodos mágicos que puedes usar para potenciar tus habilidades de programación en Python.
