Nuestra propia Colección en Python


Está muy bien conocer y utilizar los objetos de tipo colección de Python “incorporados” (en la documentación lo leeremos en inglés como “Built-in”) en Python (o casi cualquier otro lenguaje de programación), como pueden ser Listados (“List”), Diccionarios (“Dict”; también conocidos como Mapas Hash o “HashMap”), Conjuntos (“Set”) o Tuplas (“Tuple”); pues nos aseguran una alta optimización al estar escritos a bajo nivel dentro del interprete.

En resumen, una “Colección”, como su nombre indica, colecciona “cosas”, es decir, guarda datos y los organiza para su uso con alguna estructura de datos (estructuras de datos como puede ser datos por orden de inserción, por claves, en forma de árbol, etc.).

Pero en muchas ocasiones es mejor crear nuestra propia Colección bien por limpieza, reutilización de código, por comodidad, porque no existe lo que necesitamos o incluso porque va a ser más óptimo que utilizar otras estructuras de datos.

¿Acaso hay algo que sea necesario fuera de las estructuras “incorporadas” en Python? Por ejemplo, un diccionario nunca guarda el orden de inserción de los elementos, podríamos necesitar un diccionario y que además guarde el orden de los elementos insertados ¿Y no valdría con un Listado que guarde las claves por un lado en orden y el Diccionario que guarde las claves y los valores por otro? Ya tendríamos que usar dos estructuras de datos y mantenerlas (cuando se inserte en una insertar en la otra, o cuando se borre de una borrar de la otra), si este comportamiento lo necesitamos varias veces con crear una estructura de tipo Colección que haga las cosas por sí misma y reutilizarla nos ahorraríamos mucho código. Por cierto, esta estructura de datos existe dentro del paquete “Collections” de Python, se llama “OrderedDict” que podemos encontrar en https://docs.python.org/3/library/collections.html. También existen otras tantas estructuras de datos dentro de la misma biblioteca “Collections” que te recomiendo que eches un vistazo porque seguramente sean muy útiles para diferentes propósitos.

¿Y qué pudiéramos crear que no exista dentro del paquete “Collections” de Python? Es echarle imaginación o por la necesidad. Por ejemplo, se me ocurre un Listado que guarde sus elementos en un fichero y no en memoria RAM (pues un Listado que ocupe cientos de Gibibytes puede que no quepa en la memoria RAM de tu ordenador, y sí que cabe si lo guarda a fichero, es decir al disco duro); o simplemente un diccionario que solo permita guardar claves con “nombres de pasteles”.

Nota sobre la versión de Python utilizada: Aquí pondré código para la versión 3 de Python que cambia en unos pocos detalles sobre la versión 2. Pero explicaré como cambiar el código para que funcione también en la versión 2.

Código en GitHub (Recomiendo primero leer el artículo y descargar/clonar el código al final): https://github.com/Invarato/JarrobaPython/tree/master/nuestra_propia_coleccion

Si entiendes qué es un Listado de Python ya entiendes muchas cosas

Para poder diseñar nuestra propia Colección primero tenemos que entender cómo funciona una Colección cualquiera.

Por ejemplo, un Listado (“List”) en Python (Recordatorio: en Python delimitar los Strings con comillas dobles “” o comillas simples ‘’ es indiferente; aquí utilizo generalmente comillas simples para no sobrecargar el código y facilitar su lectura).

mi_listado = ['PRIMERO', 'SEGUNDO', 'TERCERO']

¿Qué ventaja tiene respecto otras cosas? Se le puede preguntar por cuántos elementos tiene dentro, por lo que es Medible (En inglés “Sized”).

cantidad_elementos = len(mi_listado)
print('Cantidad de elementos en mi listado: {}'.format(cantidad_elementos))

Muestra por pantalla (Nota sobre la concatenación de cadenas de caracteres: Utilizo la función “format()”, que simplemente se le pasan valores y sustituye en orden las llaves “{}” que encuentra en el String anterior a esta función):

Cantidad de elementos en mi_listado: 3

Se le puede preguntar por si tiene algún valor dentro de su contenido, por lo que es Contenedor (“Container”).

if 'SEGUNDO' in mi_listado:
    print('Existe en mi_listado')
else:
    print('NO existe en mi_listado')

Nos muestra:

Existe en mi_listado

Y como se puede iterar (recorrer) elemento a elemento, entonces es Iterable (En inglés o en español se dice igual “Iterable”).

for valor in mi_listado:
    print('Elemento del listado: {}'.format(valor))

Que imprime:

Elemento del listado: PRIMERO
Elemento del listado: SEGUNDO
Elemento del listado: TERCERO

Pruébalo

Es importante que lo experimentes tú mismo, por lo que te facilito el anterior código Python listo para copiar, pegar y ejecutar:

#!/usr/bin/env python
# -*- coding: utf-8 -*-


if __name__ == "__main__":
    mi_listado = ['PRIMERO', 'SEGUNDO', 'TERCERO']

    # Es Medible
    cantidad_elementos = len(mi_listado)
    print('Cantidad de elementos en mi_listado: {}'.format(cantidad_elementos))

    # Es Contenedor
    if "SEGUNDO" in mi_listado:
        print('Existe en mi_listado')
    else:
        print('NO existe en mi_listado')

    # Es Iterable
    for valor in mi_listado:
        print('Elemento del listado: {}'.format(valor))

Por lo que en Python (y en otros lenguajes de programación), nos ayudan separándonos cada una de estas cosas en clases que podremos heredar para extender su comportamiento (como veremos más adelante). Tenemos la documentación en https://docs.python.org/3/library/collections.html y https://docs.python.org/3/library/collections.abc.html.

¿En la anterior imagen que es lo que está en medio gris y con rayas a ambos lados?

Los métodos del tipo __XXX__ (el nombre del método con dos guiones bajos a ambos lados) son los llamados “métodos mágicos” en Python. Realmente son métodos que trae Python y que hacen alguna función “especial” (mágica) que podremos sobrescribir para utilizar todo “el poder de su magia”. Por ejemplo __len__, nos permitirá de manera sencilla que al pasar un objeto (la instancia de una clase que bien podría estar diseñada por nosotros) al método de Python “len()” nos devuelva la cantidad de elementos que tiene el objeto (por ejemplo podríamos hacer algo así: len(mi_objeto_de_mi_propia_clase) ). Es decir, podremos utilizar cualquier cosa (método “len()”, el bucle “for”, el condicional “if … in”, etc.) de igual manera a como lo hace el código nativo Python ¡Magia! (amplío la explicación de los “métodos mágicos” más abajo).

Vamos a crear nuestro primer objeto que utilice esta “magia”. Para hacerlo correctamente en Python, vamos a importar la biblioteca “collections” que trae las diferentes interfaces que necesitaremos para diseñar nuestra propia Colección (Se podría hacer igualmente sin importar este interfaz, pero perderíamos varias ayudas al desarrollo y no programaríamos bien; por ejemplo, ningún IDE nos ayudará a crear la clase correctamente; tampoco funcionaría si en algún otro sitio de nuestro código quisiéramos preguntar si el objeto es algo de la clase, por ejemplo: isinstance(objeto_que_podria_ser_iterable, abc.Iterable) ).

Contenedor (Container)

Vamos a empezar programando un Contenedor. Para ello nuestra clase que llamaremos “Contenedor” tiene que heredar de “abc.Container”:

from collections import abc


class Contenedor(abc.Container):

Lo que hay que importar en Python 2 (y antes de Python 3.3, pues hicieron el cambio desde la versión 3.3 en adelante): NO hay que importar «from collections import abc» sino «import collections«. Por tanto, NO hay que heredar de «abc» sino «collections«; es decir, en vez de escribir «class Contenedor(abc.Container)» hay que escribir «class Contenedor(collections.Container)«

Si estamos utilizando algún IDE, detectará que estamos utilizando la Interfaz para nuestra clase pero que le falta implementar los métodos abstractos (La siguiente imagen es del IDE PyCharm, pero cualquier otro nos dirá algo parecido).

Sino utilizamos IDE, podremos ejecutar nuestro programa Python y nos mostrará el siguiente error:

TypeError: Can't instantiate abstract class Contenedor with abstract methods __contains__

De una manera u otra nos ayuda a que no se nos olvide implementar los métodos necesarios para que sea una clase de tipo “Container”. En este caso, el método abstracto que tenemos que sobrescribir es: __contains__

from collections import abc


class Contenedor(abc.Container):

    def __contains__(self, x):
        pass

Si dejamos que nos lo genere automáticamente el IDE nos creará algo más parecido al siguiente código:

from collections import abc


class Contenedor(abc.Container):

    def __contains__(self, x: object) -> bool:
        pass

Si estamos trabajando con Python 3, el IDE nos creará el método incluyendo el tipado de parámetros y de retorno (El tipado en Python es opcional. Para Python 2, no se puede definir el tipado de este modo. De cualquier manera, se defina el tipado o no, tanto en Python 2 como en Python 3, el método __contains__ aceptará de entrada un parámetro de cualquier tipo, es decir «object», y deberá retornar siempre un valor de tipo “bool”). En los siguientes ejemplos quitaré los tipos para que sean más simples de entender.

El método __contains__ nos va a servir para preguntar directamente con un “if <algo> in <Contenedor>” (con la sintaxis simplista de Python) si nuestro objeto tiene algo o no. Por ejemplo:

mi_contenedor = Contenedor()

if "algo" in mi_contenedor:
    print('Existe en mi_contenedor')
else:
    print('NO existe en mi_contenedor')

En este ejemplo preguntamos si el texto “algo” lo contiene en nuestro objeto “mi_contenedor” que es de la clase “Contenedor”. Este “if <algo> in <Contenedor>” pregunta directamente al método __contains__ y le va a pasar el texto “algo” (es decir, “if <algo> in <Contenedor>” pasa como parámetro «x» el valor «algo» al «método mágico» __contains__ del objeto «Contenedor») para que la clase “Contenedor” haga algo con eso y devuelva un valor «bool» (“True” o “False”), que es lo que espera el “if”.

Por ejemplo, si se le pasa el texto “algo” que devuelva que “sí lo contiene”.

from collections import abc


class Contenedor(abc.Container):

    def __contains__(self, x):
        if x == 'algo':
            return True
        else:
            return False

De este modo, si ejecutamos lo anterior nos imprimirá:

Existe en mi_contenedor

Pruébalo

Te facilito el código entero para que lo puedas ejecutar:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from collections import abc


class Contenedor(abc.Container):

    def __contains__(self, x):
        if x == 'algo':
            return True
        else:
            return False


if __name__ == "__main__":
    mi_contenedor = Contenedor()

    if "algo" in mi_contenedor:
        print('Existe en mi_contenedor')
    else:
        print('NO existe en mi_contenedor')

Medible (Sized)

Otra “cosa mágica de Python”, poder preguntar a un objeto y que nos devuelva cuántas “cosas” (por lo general la cantidad de datos) que tiene dentro nuestro objeto.

mi_medible = Medible()

cantidad = len(mi_medible)

print('Cantidad de elementos en mi_medible: {}'.format(cantidad))

Pasar un objeto al método len() de Python y este método len() buscará que esté implementado el método __len__ que se encuentra en el interfaz “Sized”. El método __len__ retorna un número entero positivo.

from collections import abc


class Medible(abc.Sized):

    def __len__(self):
        return 100

Imprimirá:

Cantidad de elementos en mi_medible: 100

Pruébalo

Este método __len__ es muy sencillo, si has programado algo en Python ya estarás harto de utilizar len() para obtener la cantidad de lo que sea (listados, diccionarios, conjuntos, etc.). Por eso te voy a regalar un código un poco más realista, para probarlo y ver su utilidad.

Nuestra siguiente clase Medible va a guardar un texto y guardará en otra variable la cantidad de letras. Si se le añade una letra (con el método “add_letra()” que crearemos)  tiene que actualizar tanto el texto guardado como la variable que guarda la cantidad de letras. Cuando se llame a len(), que llamará al método __len__ de nuestra clase, devolverá simplemente la variable que guarda la cantidad de letras del objeto (como puedes comprobar no hace falta calcularla, ya está calculada a medida que se añaden letras con “add_letra()”). He añadido un método extra “get_texto()” para obtener el String que está guardado en nuestro objeto.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from collections import abc


class Medible(abc.Sized):

    def __init__(self):
        self.cantidad_de_letras = 3
        self.texto_con_letras = "ABC"

    def add_letra(self, letra):
        self.texto_con_letras += letra
        self.cantidad_de_letras += 1

    def __len__(self):
        return self.cantidad_de_letras

    def get_texto(self):
        return self.texto_con_letras


if __name__ == "__main__":
    mi_medible = Medible()

    texto = mi_medible.get_texto()
    cantidad_letras_del_texto = len(mi_medible)

    print('mi_medible tiene {} letras en el texto: "{}"'.format(cantidad_letras_del_texto, texto))

    mi_medible.add_letra("D")

    texto = mi_medible.get_texto()
    cantidad_letras_del_texto = len(mi_medible)

    print('mi_medible tiene {} letras en el texto: "{}"'.format(cantidad_letras_del_texto, texto))

Muestra por pantalla:

mi_medible tiene 3 letras en el texto: "ABC"
mi_medible tiene 4 letras en el texto: "ABCD"

Iterable

Y la última clase que nos interesa implementar es el método __iter__ de “Iterable”. He querido dejar este para el final porque es un poco especial, aunque sencilla de entender en un par de minutos que tardarás en leer esta parte 😉

Fíjate como se itera por un bucle “for” (arriba te puse un ejemplo con el listado). Pues queremos lo mismo, pasarle al bucle “for” el objeto de mi clase y que se itere por cada elemento que exista dentro de mi objeto.

mi_iterable = Iterable()

for valor in mi_iterable:
    print('Elemento de mi_iterable: {}'.format(valor))

Queremos que nos devuelva uno a uno todos los valores dentro de nuestro objeto. Para este ejemplo vamos a devolver lo mismo que el ejemplo del listado que vimos previamente (va a tener tres valores: “PRIMERO”, “SEGUNDO” y “TERCERO”), y también queremos que nos pinte en la consola:

Elemento de mi_iterable: PRIMERO
Elemento de mi_iterable: SEGUNDO
Elemento de mi_iterable: TERCERO

¿Cómo se lo ordenamos al objeto Iterable? El bucle “for <valor iterado> in <Iterable>” siempre va a buscar el método __iter__ del “Iterable”, y el bucle “for” va a esperar a que el método __iter__ le devuelva un objeto de tipo “Iterador” (En inglés “Iterator”).

“Iterador” vs “Iterable”

“Iteardor” e “Iterable”, dos cosas tan diferentes que se confunden como iguales. La diferencia rápida es que un «Iterador» crea cada elemento a medida que se le pide, por eso solo se puede iterar por él una única vez (es decir, si iteras la primera vez a un «Iterador» con un bucle FOR podrás obtener los elementos, pero si con otros bucles FOR posteriores intentas iterar al «Iterador» no podrás obtener sus elementos pues ya habrán sido creados y devueltos la primera vez); sin embargo, un «Iterable» se puede iterar por los elementos más de una vez ya que tiene guardados los elementos por los que se necesiten iterar, o el «iterable» la primera vez itera al «Iterador» para guardar los elementos que crea y si se itera otras veces se utilizarán los elementos guardados (por eso un listado, que es un «iterable», se puede iterar las veces que queramos en todos los bucles FOR que necesitemos).

De cualquier manera la explicación completa de “Iteardor” e “Iterable” es un poco cíclica. Cíclica pues el método __iter__ de una clase “Iterable” devuelve un “Iterador”, pero la clase “Iterador” hereda de “Iterable”.

Es decir, la clase padre devuelve un objeto de la clase hija, pero la hija implementa el método __iter__ de la clase padre por lo que se devuelve a sí misma.

¿Cómo? Veamos un momento la estructura que tendríamos que implementar tanto de “Iterable” como de “Iterador”.

from collections import abc


class Iterable(abc.Iterable):

    # Método de la clase padre (Iterable) que retorna un objeto de la clase hija (Iterator)
    def __iter__(self):
        return un_iterador


class Iterador(abc.Iterator):

    # Método de la clase hija (Iterador), necesario para crear un Iterator
    def __next__(self):
        pass  # Veremos cómo funciona más adelante

    # Método de la clase padre (Iterable) que retorna un objeto de la clase hija (Iterator)
    def __iter__(self):
        return un_iterador

¿Pero por qué? ¿Qué clase de locura es esta? No es una locura, tiene su lógica, y es bastante simple. Veámoslo con un ejemplo funcional.

Vamos a empezar por la clase hija, el “Iterador”, por lo que tenemos que implementar como mínimo el «método mágico» __next__, que retorna un valor de nuestra estructura de datos y cada vez que se llame a __next__ hay que asegurarse que devuelva el siguiente valor (En el listado simple [“PRIMERO”, “SEGUNDO”, “TERCERO”], queremos que devuelva:

  1. Al llamar a __next__ la primera vez, tiene que devolver el valor “PRIMERO”
  2. Cuando se vuelva a llamar a __next__ una segunda vez, el valor “SEGUNDO”
  3. Y una tercera vez a __next__, el valor “TERCERO”
  4. En casos de que se llamara a __next__ más veces que la cantidad de valores que hay en nuestro objeto, hay que dar “un aviso de Detener la iteración”, pues ya no quedan valores que devolver.

Nuestros ingredientes son un método llamado __next__ dentro de una clase. Pues lo mejor es guardar una variable de la clase que vaya contando en donde se ha quedado (en el siguiente ejemplo se llama “siguiente_valor_a_devolver”), y así cada vez que alguien llame a __next__ devolverá cada uno de los valores en el orden que queríamos. Al no quedar más valores lanzaremos la excepción especial “StopIteration” para avisar a quién itere a nuestro Iterador que ya no quedan más valores que devolver.

from collections import abc


class Iterador(abc.Iterator):

    def __init__(self):
        self.siguiente_valor_a_devolver = 0

    def __next__(self):
        self.siguiente_valor_a_devolver += 1

        if self.siguiente_valor_a_devolver == 1:
            return "PRIMERO"
        elif self.siguiente_valor_a_devolver == 2:
            return "SEGUNDO"
        elif self.siguiente_valor_a_devolver == 3:
            return "TERCERO"
        else:
            raise StopIteration

Y podemos probarlo llamando sucesivamente al método __next__ del objeto “Iterador” (fíjate que he envuelto las llamadas a __next__ en un “try/except”, porque sabemos que en algún momento va a lanzar la excepción “StopIteration”):

mi_iterador = Iterador()

try:

    # PRIMERO
    str_val_conv = mi_iterador.__next__()
    print('__next__ devuelve: "{}"'.format(str_val_conv))

    # SEGUNDO
    str_val_conv = mi_iterador.__next__()
    print('__next__ devuelve: "{}"'.format(str_val_conv))

    # TERCERO
    str_val_conv = mi_iterador.__next__()
    print('__next__ devuelve: "{}"'.format(str_val_conv))

    # StopIteration
    str_val_conv = mi_iterador.__next__()
    print('No llega a este print por la excepcion StopIteration: "{}"'.format(str_val_conv))

except StopIteration:
    print('La ultima llamada a __next__ ha lanzado la excepcion StopIteration')

Muestra al ejecutarse:

__next__ devuelve: "PRIMERO"
__next__ devuelve: "SEGUNDO"
__next__ devuelve: "TERCERO"
La ultima llamada a __next__ ha lanzado la excepcion StopIteration

Vale, ya tenemos el “Iterador”, ahora el “Iterable”.

El “Iterable” necesita implementar el método __iter__ que devuelve un “Iterador”. Como tenemos la clase “Iterador” de antes, es muy sencillo, la podemos simplemente instanciar y devolver.

from collections import abc


class Iterable(abc.Iterable):

    def __iter__(self):
        mi_iterador = Iterador()
        return mi_iterador

De este modo, el bucle “for <valor iterado> in <Iterable>” puede obtener de un «Iterable» un objeto de tipo “Iterador” al que ir obteniendo paso a paso (__next__ a __next__) los valores contenidos (que en la sintaxis “for <valor iterado> in <Iterable>” se pasarán en «<valor iterado>»). El bucle termina de iterar cuando recoge del “Iterador” la excepción “StopIteration” (es decir, el bucle “for <valor iterado> in <Iterable>” nos ha resumido todo el código anterior de ir mi_iterador.__next__() a mi_iterador.__next__() hasta la excepción “StopIteration”).

mi_iterable = Iterable()

for valor in mi_iterable:
    print('Elemento de mi_iterable: {}'.format(valor))

Imprime:

Elemento de mi_iterable: PRIMERO
Elemento de mi_iterable: SEGUNDO
Elemento de mi_iterable: TERCERO

Una vez entendido lo anterior ¿No te parece muchas clases para que el bucle “for” itere por nuestro “Iterable”? Sí, y es la razón por la que el propio “Iterador” se puede devolver así mismo, de esta manera nos ahorramos la clase “Iterable” (que para eso la hereda en este extraño ciclo “Iterador”). Las dos clases “Iterador” e “Iterable” en una sola:

from collections import abc


class IteradorIterable(abc.Iterator):

    def __init__(self):
        self.siguiente_valor_a_devolver = 0

    def __next__(self):
        self.siguiente_valor_a_devolver += 1

        if self.siguiente_valor_a_devolver == 1:
            return "PRIMERO"
        elif self.siguiente_valor_a_devolver == 2:
            return "SEGUNDO"
        elif self.siguiente_valor_a_devolver == 3:
            return "TERCERO"
        else:
            raise StopIteration

    def __iter__(self):
        return self

Pasamos el objeto al bucle “for”.

mi_iterador_iterable = IteradorIterable()

for valor in mi_iterador_iterable:
    print('Elemento de mi_iterador_iterable: {}'.format(valor))

Y resulta que nos devuelve lo mismo con menos código:

Elemento de mi_iterador_iterable: PRIMERO
Elemento de mi_iterador_iterable: SEGUNDO
Elemento de mi_iterador_iterable: TERCERO

En resumen, hemos tenido que entender tres cosas:

  • Iterador: devuelve de uno en uno (__next__ a __next__) cada valor a medida que se lo pedimos. Un «Iterador» crea cada elemento a medida que se le pide, por eso solo se puede iterar por él una única vez (es decir, si intentas iterar varias veces con un bucle FOR, la segunda vez ya estará iterado y no se podrá volver a iterar).
  • Iterable: devuelve un objeto Iterador (con __iter__). Un «Iterable» se puede iterar por los elementos más de una vez (por eso un listado se puede iterar varias veces al utilizarlo en varios bucles FOR).
  • Iterador que es Iterable: combina ambas clases anteriores para simplificar el código, es decir, tiene __next__ e __iter__.

Interior de un bucle “For” en Python

El bucle “for <valor iterado> in <Iterable>” hace su magia cuando le pedimos que itere por un “objeto Iterable” que le pasamos (bien pudiera ser la típica lista, un diccionario, un set, o cualquier otro Iterable), pero en su interior sigue unos pasos que ya hemos visto y aquí te amplío. Por ejemplo:

mi_lista = [1, 2, 3, 4, 5]

for valor in mi_lista:
    print('Elemento de mi_ lista: {}'.format(valor))
 
# Continua el código después del bucle for ...

Lo que hace dentro del bucle “for” es llamar al método __iter__() del objeto Iterable que le hemos pasado (en el ejemplo anterior “mi_lista”) para obtener el “objeto Iterador”. Luego va llamando una y otra vez al método __next__() del “objeto Iterador” devolviendo el valor al interior del cuerpo del bucle “for” (del ejemplo anterior, “valor” y lo imprime con “print”), hasta que en una llamada al __next__() del “objeto Iterador” recibe la excepción «StopIteration» y termina el bucle “for” (en el ejemplo anterior llegaría al comentario “# Continua el código después del bucle for”).

Como desarrolladores hemos visto es muy sencillo utilizar el bucle “for”, tampoco es muy difícil crear al bucle “for”, por ejemplo, podríamos simularlo con “while” todos los anteriores pasos.

Con un bucle “while” tendríamos que hacer a mano todo lo que nos ahorra el bucle “for”. Creo que ayuda a comprender la abstracción del “for”, y para procesos complejos es necesario recorrer con un bucle “while”.

Para obtener el “iterador”, podemos llamar directamente al método __iter__ del objeto “iterable”. Luego con __next__ obtener todos los valores hasta que se lance la excepción “StopIteration”:

mi_iterable = Iterable()
mi_iterador = mi_iterable.__iter__()

continuar = True
while continuar:
    try:
        valor = mi_iterador.__next__()
        print('__next__ devuelve: "{}"'.format(valor))
    except StopIteration:
        print('La ultima llamada a __next__ ha lanzado la excepcion StopIteration, que sirve para salir del bucle WHILE')
        continuar = False

Imprime por pantalla:

__next__ devuelve: "PRIMERO"
__next__ devuelve: "SEGUNDO"
__next__ devuelve: "TERCERO"
La ultima llamada a __next__ ha lanzado la excepcion StopIteration, que sirve para salir del bucle WHILE

Hemos creado al bucle “for” con nuestras propias manos 😀

Colección (Collection)

Volvamos con el ejemplo más sencillo de “Colección”, el “Listado de Python”.

Repasemos rápidamente que es lo que se puede hacer con un objeto “Listado de Python”

  • Contiene elementos (“Container”)
my_listado = [“valor 1”, “valor 2”, “valor 3”]
  • Se puede iterar (“Iterable”)
for valor in mi_listado:
    # Iterar por los valores de mi_listado
  • Se puede medir
cantidad_elementos = len(mi_listado)

Es decir, la “Lista de Python” hereda para implementar las tres clases anteriores “Iterable”, “Container” y “Sized”.

Podríamos aplicar la herencia múltiple de Python, pero no es lo más eficaz.

Como hay varias cosas que heredan de “Iterable”, “Container” y “Sized” a la vez se agrupan en la clase “Collection”.

Con lo que bastaría heredar de “Collection” para implementar correctamente una Colección.

from collections import abc


class Coleccion(abc.Collection):

    # Implementado desde la clase Sized
    def __len__(self):
        pass

    # Implementado desde la clase Container
    def __contains__(self, x):
        pass

    # Implementado desde la clase Iterable
    def __iter__(self):
        pass

Como tiene que ser se ha añadido el método __iter__ que devuelve un objeto de tipo “Iterador”. Como hemos visto poco antes, lo más probable será queramos hacer todo en la misma clase por ahorrarnos código, por lo que podemos heredar de “Iterator” e implementar el método __next__.

from collections import abc


class MiColeccionIterador(abc.Collection, abc.Iterator):

    def __len__(self):
        pass

    def __iter__(self):
        pass

    def __contains__(self, x):
        pass

    # Método de abc.Iterator
    def __next__(self):
        pass

Pruébalo

Un ejemplo rápido ampliando uno de los anteriores. Crearemos nuestro propio “Listado inmutable”, es decir, tendrá datos dentro que no podremos cambiar.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from collections import abc


class MiColeccionIterador(abc.Collection, abc.Iterator):

    def __init__(self):
        self.siguiente_valor_a_devolver = 0

    def __len__(self):
        return 3

    def __contains__(self, x):
        return x is "PRIMERO" or x is "SEGUNDO" or x is "TERCERO"

    def __next__(self):
        self.siguiente_valor_a_devolver += 1

        if self.siguiente_valor_a_devolver == 1:
            return "PRIMERO"
        elif self.siguiente_valor_a_devolver == 2:
            return "SEGUNDO"
        elif self.siguiente_valor_a_devolver == 3:
            return "TERCERO"
        else:
            raise StopIteration

    def __iter__(self):
        self.siguiente_valor_a_devolver = 0
        return self


if __name__ == "__main__":

    mi_coleccion_iterador = MiColeccionIterador()

    print('mi_coleccion_iterador tiene {} elementos'.format(len(mi_coleccion_iterador)))

    if "ALGO" in mi_coleccion_iterador:
        print('"ALGO" Existe en mi_coleccion_iterador')
    else:
        print('"ALGO" NO existe en mi_coleccion_iterador')

    if "SEGUNDO" in mi_coleccion_iterador:
        print('"SEGUNDO" Existe en mi_coleccion_iterador')
    else:
        print('"SEGUNDO" NO existe en mi_coleccion_iterador')

    for valor in mi_coleccion_iterador:
        print('Elemento de mi_coleccion_iterador: {}'.format(valor))

Imprime:

mi_coleccion_iterador tiene 3 elementos

"ALGO" NO existe en mi_coleccion_iterador
"SEGUNDO" NO existe en mi_coleccion_iterador

Elemento de mi_coleccion_iterador: PRIMERO
Elemento de mi_coleccion_iterador: SEGUNDO
Elemento de mi_coleccion_iterador: TERCERO

Nota sobre la inicialización del Iterador: Cada vez que se llame a __iter__ nos puede interesar que vuelva a empezar desde el principio a devolver cosas el objeto “Iterador” mediante el método __next__ (de ahí que en __iter__ hayamos puesto “self.siguiente_valor_a_devolver = 0” en este ejemplo). Sino volvemos a iniciar a “0” la variable “self.siguiente_valor_a_devolver”, la primera vez que utilicemos el objeto “Iterador” en un bucle “for” va a funcionar, pero la variable que guarda el paso del siguiente __next__ ya habrá llegado al final. Si posteriormente queremos llamar a nuestro “Iterador” en otros bucles “for”, entonces __next__ lanzará siempre StopIteration y no entrará en los cuerpos de los bucles “for”; sí entrará si se inicializa la variable “self.siguiente_valor_a_devolver” a “0”, pues volverá a iterar desde el principio.

Ciclo de vida de los métodos de una Colección (Collection)

Tan importante conocer lo anterior como el ciclo de vida que sigue cada vez que llamamos a un método de nuestra clase Colección (Collection), pues nos dará las pistas para poder implementar nuestro código de una forma correcta. Los que hemos visto son:

  • Obtener un valor dada una clave ( valor = collection[«clave»] ) llama a __getitem__.
  • if «clave» in collection: llama al método mágico __contains__ para preguntar si tiene la clave.
  • for clave in collection: llama a __iter__, e __iter__ llama a __next__ para que le devuelva cada clave.
  • len(collection) llama a __len__.

En perspectiva

Termino dejándote todas las relaciones juntas de lo que hemos visto en este artículo:

Hemos aprendido mucho sobre “Colecciones” (“Collection”), todavía queda la especialización de nuestras “Colecciones”, además de las capacidades tan increíbles como la mutabilidad (capacidad de cambiar con el típico “añadir dato” o “eliminar dato” de nuestra “Colección”) entre otras que veremos en un artículo futuro.

Bibliografía

Comparte esta entrada en:
Safe Creative #1401310112503
Nuestra propia Colección en Python por "www.jarroba.com" esta bajo una licencia Creative Commons
Reconocimiento-NoComercial-CompartirIgual 3.0 Unported License.
Creado a partir de la obra en www.jarroba.com

4 thoughts on “Nuestra propia Colección en Python”

  1. Buenas tardes Ramón, he estado buscando algo, por no decir igual, similar a lo que planteas; la verdad es que necesito crear una clase que se llame Parqueadero y sea un contendor de objetos del tipo Auto. Si a la clase le hago heredar de «list» ya debería tener los métodos de list y a su vez debería implementar los llamados métodos mágicos. O estoy equivocado y debería declarar una variable de tipo list y asignar ahí cuanto auto entre o salga del Paqueadero? Este segundo método parecería ilógico, pues el mismo modelo tendría que iterar sobre la variable lista asignada y no sobre sí mismo…Por ejemplo, debo encontrar un auto por su número de placa, para poder usar la forma xPlca in p, siendo p la instancia del parqueadero y xPlaca la placa que voy a buscar en el parqueadero… igual se debo ingresar un auto, debería hacerlo como p.append(car1) o debo implementar el metodo __append__ ? Ahora bien, si digo p.append implica self.append o debo hacerlo en realidad declarando una variable del tipo lista? O tal vez no entiendo bien el concepto de herencia

    1. Buenas Carlos.
      Si creas una clase Parqueadero que herede por ejemplo de Sequence (ver https://jarroba.com/especializa-tu-propia-coleccion-en-python/ y https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) o más concretamente de list, yendrás que implementar los métodos de Sequence o list (__setitem__, __getitem__, append, etc.) y tu clase Parqueadero ya actuará como un listado (no tendrías que usar una variable list), es decir, podrás llamar al objeto (siendo p un objeto de la clase Parqueadero): p[57]=car1, p.append(car1), car=p[57], etc.
      Un ejemplo parecido para lo que necesitas es el ejemplo de mapping en https://jarroba.com/mapeo-mapping-y-mapeo-mutable-mutablemapping-en-python/

Deja una respuesta

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies

ACEPTAR
Aviso de cookies