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.

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, collections.Iterable) ).

Contenedor (Container)

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

import collections


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__

import collections


class Contenedor(collections.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:

import collections


class Contenedor(collections.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”.

import collections


class Contenedor(collections.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 -*-
import collections


class Contenedor(collections.Container):

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


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

    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.

import collections


class Medible(collections.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 -*-
import collections


class Medible(collections.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 diferentes, cuya explicación 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”.

import collections


class Iterable(collections.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(collections.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.

import collections


class Iterador(collections.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.

import collections


class Iterable(collections.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:

import collections


class IteradorIterable(collections.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.
  • Iterable: devuelve un objeto Iterador (con __iter__).
  • 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.

import collections


class Coleccion(collections.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__.

import collections


class MiColeccionIteradora(collections.Collection, collections.Iterator):

    def __len__(self):
        pass

    def __iter__(self):
        pass

    def __contains__(self, x):
        pass

    # Método de collections.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 -*-
import collections


class MiColeccionIterador(collections.Collection, collections.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_contenedor:
        print('"ALGO" Existe en mi_coleccion_iterador')
    else:
        print('"ALGO" NO existe en mi_coleccion_iterador')

    if "SEGUNDO" in mi_contenedor:
        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.

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

Deja un comentario

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