Métodos especiales en python, Métodos especiales en Python – Endulza tus clases

Métodos especiales en Python – Endulza tus clases

Métodos especiales en Python pueden ayudarte a mejorar mucho tu código, incrementa tus habilidades con esta información.

Métodos especiales

En Python, los métodos especiales son un conjunto de métodos predefinidos que puede usar para enriquecer sus clases. Son fáciles de reconocer porque comienzan y terminan con guiones bajos dobles, por ejemplo __init____str__. Estos métodos también son con conocidos como “dunders”, una abreviación de double underline.

Los métodos Dunder nos permiten emular el comportamiento de los objetos integrados en Python. Por ejemplo, para obtener la longitud de un string puedes usar len(‘hola mundo’)

Pero cuando creamos una clase no tiene este comportamiento por defecto, ya que Python no sabría como manipular tu clase o cuál es su longitud según el ejemplo anterior.


class ClaseSimple:
    pass

>>> obj = ClaseSimple(5, 3)
>>> len(obj)
TypeError :  "el objeto de tipo 'ClaseSimple' no tiene len ()"

Para solucionar este error usamos el método especial __len__:


class ClaseSimple:

    def __len__(self):
        return 10

>>> obj = ClaseSimple(5, 3)
>>> len(obj)
10

Este elegante diseño se conoce como el modelo de datos de Python y permite a los desarrolladores aprovechar funciones del lenguaje como secuencias, iteración, sobrecarga de operadores, acceso a atributos, etc.

Para demostrar  de los métodos especiales crearé una clase Almacen que tendrá varias funcionalidades en conjunto con las funciones de Python.

Inicialización: método __init__

Necesito inicializar mi clase con el método __init__ para establecer los valores de la instancia.


class Almacen:
    """ Clase para administrar un almacen simple """
    
    def __init__(self, propietario, espacios = 0):
        """ 
        Construcctor que nos permite inicializar el objeto 
        """
        self.propietario = propietario
        self.espacios = espacios
        self.productos = []

El constructor se encarga de inicializar el objeto, recibe los datos necesarios para hacer la clase funcional. Tenemos 3 atributos:

  • propietario: Dueño del almacén
  • espacios: Cantidad de espacios en total que tiene el almacén para guardar productos.
  • productos: Lista de productos que ocuparán un espacio en el almacén.

Ahora podemos usar la clase y crear instancias:


>>> a = Almacen('Roberto') # Valor de espacios por defecto "0"
>>> a = Almacen('Mario', 32)

Representación del objeto: __str__,__repr__

Es una práctica recomendada crear una representación de los objetos en texto. Hay 2 formas de hacer esto con métodos especiales:

  • __str__: Es una representación orientada al usuario final, es la manera “informal” de representarlo
  • __repr__: Es una representación “formal” de cómo se genera el objeto.

class Almacen:
    
    # Métodos anteriores (ver arriba) ...
        
    def __str__(self):
        return 'Almacen de {} con {} espacios'\
                .format(self.propietario, self.espacios)
                
    def __repr__(self):
        return 'Almacen('{}', {})'.format(self.propietario, 
                                       self.espacios)

Ahora tenemos varias maneras de representar a nuestro objeto…


>>> a = Almacen('Mario', 32)

>>> str(a)
"Almacen de Mario con 32 espacios"

>>> print(a)
"Almacen de Mario con 32 espacios"

>>> repr(a)
"Almacen('Mario', 32)"

Funcionalidad de iteración:
__len__, __getitem__, __reversed__

Para añadir una funcionalidad de iteraciones necesito añadir primero algunos productos al objeto, crearé un método en la clase para hacerlo. Será una función simple que verifique si aún hay espacios en el almacén para poder añadirlo.


def guardar_producto(self, producto):
    if len(self.productos) + 1 > self.espacios:
        raise Exception('Ya no hay espacios para \
                        guardar productos')
    self.productos.append(producto)

Ahora añadiré productos al almacén de Mario…


alm = Almacen("Mario", 5)
alm.guardar_producto("TV")
alm.guardar_producto("Smartphone")
alm.guardar_producto("DVD")
alm.guardar_producto("Xbox")

Listo, ahora… ¿Qué podemos hacer con esta información? Se me ocurren algunas cosas básicas que serán necesarias para interactuar con este objeto:

  1. ¿Cuántos productos tengo en el almacén?
  2. ¿Cuál es el orden de guardado de mis productos?
  3. Iterar sobre los productos

Actualmente como tenemos la clase no es posible usar las funciones de Python para saberlo…


>>> len(alm)
"TypeError: object of type 'Almacen' has no len()"

>>> for producto in alm:
...    print(producto)
"TypeError: 'Almacen' object is not iterable"

>>> alm[1]
"TypeError: 'Almacen' object does not support indexing"

Con un poco de código esto se puede arreglar, veamos como…


class Almacen:
    
    # Métodos anteriores (ver arriba) ...
        
    def __len__(self):
        return len(self.productos)
    
    def __getitem__(self, ix):
        return self.productos[ix]

Ahora el código anterior funciona completamente!


>>> len(alm)
4

>>> for producto in alm:
...    print(producto)
"TV"
"Smartphone"
"DVD"
"Xbox"

>>> alm[1]
"Smartphone"

Para iterar por los productos en el orden que fueron guardados podemos usar el método especial __reversed__


def __reversed__(self):
    return self[::-1]

>>> reversed(alm)
['Xbox', 'DVD', 'Smartphone', 'TV']

Sobrecarga de operadores: __add__

¿Alguna vez quisiste usar el operador + con algunas de tus clases? Bueno, en Python todo es un objeto, por lo que entra en juego el famoso poliformismo.

Cuando usamos el operador + se comporta muy distinto si lo usamos con un número, un string o una lista. En este momento nuestra clase Almacen no soporta el uso de este operador…


>>> alm + alm2
"TypeError:unsupported operand type(s) for +: 'Almacen' and 'Almacen'"

Para añadir esta funcionalidad a nuestra clase usamos el método especial __add__. Pero, ¿Cuál debería ser el resultado de la suma de 2 almacenes? La forma más intuitiva sería sumar todos sus atributos…


def __add__(self, otro):
    prop = "{} & {}".format(self.propietario,
                            otro.propietario)
    espacios = self.espacios + otro.espacios
    nuevo = Almacen(prop, espacios)
    nuevo.productos = self.productos + otro.productos
    
    return nuevo

>>> alm = Almacen("Mario", 5)
>>> alm2 = Almacen("Roberto", 12)
>>> alm3 = alm + alm2
>>> print(alm3)
"Almacen de Mario & Roberto con 17 espacios"

Operadores de comparación: __eq__

También podemos usar los operadores como >, >=, <=, < o ==. Con la ayuda del método __eq__ podemos hacer comparaciones lógicas con ==. 

 

¿Cuál sería el comportamiento esperado de esta comparación? Lo haré de forma que sólo si la cantidad de espacios de ambos almacenes son iguales regresará verdadero.


def __eq__(self, otro):
    return self.espacios == otro.espacios

>>> alm = Almacen("Mario", 5)

>>> alm2 = Almacen("Roberto", 12)
>>> alm == alm2
False

>>> alm2 = Almacen("Roberto", 5)
>>> alm == alm2
True

Al final nuestra clase termina luciendo así:


class Almacen:
    """ Clase para administrar un almacen simple """
    
    def __init__(self, propietario, espacios = 0):
        """ 
        Construcctor que nos permite inicializar el objeto 
        """
        self.propietario = propietario
        self.espacios = espacios
        self.productos = []
        
    def __str__(self):
        return 'Almacen de {} con {} espacios'\
                .format(self.propietario, self.espacios)
                
    def __repr__(self):
        return 'Almacen({}, {})'.format(self.propietario, 
                                       self.espacios)
        
    def __len__(self):
        return len(self.productos)
    
    def __getitem__(self, ix):
        return self.productos[ix]
    
    def __reversed__(self):
        return self[::-1]
        
    def __add__(self, otro):
        prop = "{} & {}".format(self.propietario,
                                otro.propietario)
        espacios = self.espacios + otro.espacios
        nuevo = Almacen(prop, espacios)
        nuevo.productos = self.productos + otro.productos
        
        return nuevo
    
    def __eq__(self, otro):
        return self.espacios == otro.espacios
    
    def guardar_producto(self, producto):
        if len(self.productos) + 1 > self.espacios:
            raise Exception('Ya no hay espacios para \
                            guardar productos')
        self.productos.append(producto)

Luce muy Pythonizado ¿No?  Hay muchos métodos especiales más que añaden funcionalidades geniales a tus clases.

Te dejo una lista de ellos, además puedes entrar directamente a la documentación de Python para encontrar la información oficial y más métodos especiales. Si te han gustado te recomiendo mi publicación sobre 5 Tips para programar con Python.


Operadores binarios

Operador           Método
+                  object.__add__(self, other)
-                  object.__sub__(self, other)
*                  object.__mul__(self, other)
//                 object.__floordiv__(self, other)
/                  object.__div__(self, other)
%                  object.__mod__(self, other)
**                 object.__pow__(self, other[, modulo])
<<                 object.__lshift__(self, other)
>>                 object.__rshift__(self, other)
&                  object.__and__(self, other)
^                  object.__xor__(self, other)
|                  object.__or__(self, other)

Operadores de asignación:

Operador          Método
+=                object.__iadd__(self, other)
-=                object.__isub__(self, other)
*=                object.__imul__(self, other)
/=                object.__idiv__(self, other)
//=               object.__ifloordiv__(self, other)
%=                object.__imod__(self, other)
**=               object.__ipow__(self, other[, modulo])
<<=               object.__ilshift__(self, other)
>>=               object.__irshift__(self, other)
&=                object.__iand__(self, other)
^=                object.__ixor__(self, other)
|=                object.__ior__(self, other)

Operadores unarios:

Operador          Método
-                 object.__neg__(self)
+                 object.__pos__(self)
abs()             object.__abs__(self)
~                 object.__invert__(self)
complex()         object.__complex__(self)
int()             object.__int__(self)
long()            object.__long__(self)
float()           object.__float__(self)
oct()             object.__oct__(self)
hex()             object.__hex__(self)

Operadores de comparación

Operador          Método
<                 object.__lt__(self, other)
<=                object.__le__(self, other)
==                object.__eq__(self, other)
!=                object.__ne__(self, other)
>=                object.__ge__(self, other)
>                 object.__gt__(self, other)

Conclusión

Seguro estás tentado a correr y usarlas en tus clases para presumir con tus compañeros de trabajo, el uso de métodos especiales sin duda le puede añadir mucha funcionalidad a tus clases, pero no caigas en el error de querer usarlas todas todo el tiempo, no todas las integraciones con Python pueden ser intuitivas. 

Imagina una clase Empleado que use el operador binario Empleado << 1, ¿No tiene mucho sentido verdad? En esos casos siempre es mejor usar métodos directamente con algo más descriptivo sobre lo que realiza.

 

2 thoughts to “Métodos especiales en Python – Endulza tus clases”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *