Python – Troceando desde el lado izquierdo

Objetivo: mostrar algunas técnicas avanzadas de slicing de listas in situ.

Todo pythonista que se precie debe estar familiarizado con los mecanismos básicos de slicing de secuencias y sus usos idiomáticos más comunes. Si aún no tienes claro qué significan expresiones como secuencia1[2:5] o secuencia2[::-1], puedes encontrar en el blog diversos artículos que te ayudarán a aclarar este concepto.

Dentro de la gama de secuencias de Python se encuentran las magníficas listas, que tienen una interesante propiedad que las diferencia claramente del resto de secuencias: su mutabilidad, la capacidad de ser modificadas in situ.

Si observamos, con mucha paciencia y dedicación, cómo se comportan las listas en su hábitat natural, podremos ser testigos excepcionales de conductas raramente presenciadas por el ser humano: el slicing en el lado izquierdo de una instrucción de asignación, algo que otras secuencias, debido a su inmutabilidad, no pueden exhibir.

Consideremos, por ejemplo, la lista siguiente:

lista = ['a', 'b', 'c', 'd', 'e']

Ya sabemos que la operación de trocear cualquier secuencia crea un nuevo objeto, pero no afecta al objeto original:

>>> lista[2:4]   # una rebanada de longitud 2
['c', 'd']
>>> lista
['a', 'b', 'c', 'd', 'e']

>>> lista [::-1]    #invirtiendo la lista
['e', 'd', 'c', 'b', 'a']
>>> lista
['a', 'b', 'c', 'd', 'e']

Si lo que queremos es modificar la lista original para que tome como valor el resultado del slicing, simplemente reasignamos la lista para que, a partir de ese momento, referencie al nuevo objeto:

>>> lista = lista[2:4]
>>> lista
['c', 'd']

Este es el procedimiento general para modificar cualquier tipo de secuencia, sea mutable (listas) o inmutable (tuplas y strings). Nótese que en ningún caso hay modificación in situ del objeto inicial, tan sólo referenciamos una dirección de memoria completamente diferente en la que se halla ubicada la rebanada resultante.

Rebanadas en el lado izquierdo

Como hemos dicho, las listas son unas entidades especiales; su valorada mutabilidad permite una operación muy peculiar: trocear en el lado izquierdo de una instrucción de asignación.

Presta mucha atención a lo siguiente

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> id(lista)
45699960
>>> lista[2:4] = ['C', 'D']

Hemos generado una rebanada de longitud 2 a la cual asignamos otra lista de longitud también 2. Previamente hemos recuperado la dirección de memoria referenciada por lista simplemente a título informativo. ¿Qué crees que habrá sucedido con la lista original?

>>> lista
['a', 'b', 'C', 'D', 'e']
>>> id(lista)
45699960

Hemos modificado la lista original directamente in situ. Observa que se mantiene la misma referencia de memoria.

Guarda bien esta técnica para cuando necesites modificar con una sola operación un bloque contiguo de elementos en una lista.

¿Qué ocurre cuando la longitud de la rebanada difiere de la longitud de la lista que asignamos en el lado derecho? Supongamos que queremos meter tres elementos en un hueco de sólo dos:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista[2:4] = ['C', 'D', 'E']
>>> lista
['a', 'b', 'C', 'D', 'E', 'e']

La lista acomoda perfectamente en el hueco los tres elementos, sustituyendo los apropiados y empujando los elementos restantes, aumentando el tamaño de la lista, sin sobreescribirlos.

Es fácil entender entonces la situación contraria, cuando la longitud de la lista del lado derecho es inferior a la del troceo:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista[2:4] = ['C']
>>> lista
['a', 'b', 'C', 'e']

Llevado al extremo, si en el lado derecho ponemos una lista nula, de longitud cero, encontramos una técnica interesante que nos permite eliminar de un plumazo un bloque contiguo de elementos en una lista:

>>> lista[2:4] = []
>>> lista
['a', 'b', 'e']

En todos estos ejemplos, los dos elementos cortados originales han desaparecido. En su lugar aparecen los nuevos, extendiendo o acortando la lista según sea el caso.

Rebanadas de longitud cero

Un paso particularmente interesante resulta cuando provocamos rebanadas de longitud cero en el lado izquierdo. Una rebanada de longitud cero es aquella que devuelve una lista sin elementos. Observa un ejemplo:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista[2:2]
[]

El slicing anterior retorna todos los elementos entre el índice 2 y, sin incluir, el índice 2 también, es decir, todos los elementos comprendidos entre el índice 2 y el 1. Naturalmente, no se devuelve nada, pues estamos cortando hacia delante y no podemos retroceder. El mismo resultado se obtendría, evidentemente, con lista[2:1] o lista [2:0].

Veamos cómo se comporta en este caso la operación de asignación:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista[2:2] = ['C']
>>> lista
['a', 'b', 'C', 'c', 'd', 'e']

Como no hay nada que sustituir, el slicing nulo simplemente cumple su función de inserción en el punto indicado por el valor a la izquierda de los dos puntos.

Un error típico conceptual hubiera sido intentar algo como esto:

lista[2:2] = 'C'

La operación de slicing devuelve una lista; por lo tanto, en el lado derecho de la igualdad debe figurar una lista también:

lista[2:2] = ['C']

Naturalmente, obtendríamos el mismo resultado con el método insert:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista.insert(2, 'C')
>>> lista
['a', 'b', 'C', 'c', 'd', 'e']

Sin embargo, la técnica del slicing permite, a diferencia del método insert, insertar más de un elemento en la misma operación. El objeto list no dispone de ningún método que sea capaz de hacer esto:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista[2:2] = ['C', 'D', 'E']
>>> lista
['a', 'b', 'C', 'D', 'E', 'c', 'd', 'e']

Podemos situar también el cursor de inserción justo después del último elemento, lo que provocará que la lista se extienda por la derecha. ¿Cómo lo hacemos?

En nuestra lista de ejemplo de 5 elementos, el último tiene por índice 4, ya que el conteo empieza por cero. Para situarnos más allá de él, fuera ya de la lista, elegimos el valor siguiente, 5, que es precisamente la longitud de la lista. Es decir, de forma genérica, el valor que sitúa el cursor más allá de todos los elementos es len(lista).

Para provocar la rebanada nula en ese índice, podemos elegir, como segundo valor del slice, cualquier número entero, sin importar el que sea, pues la lista no existe a partir de ese índice. Todos estos trozos devuelven la rebanada nula a partir del último elemento:

lista[len(lista):0]
lista[len(lista):3]
lista[len(lista):5]
lista[len(lista):100]

También puede, sencillamente, obviarse, pues su omisión hace referencia, cuando recortamos hacia la derecha, al supuesto índice que habría más allá del último real:

lista[len(lista):]

Resulta más fácil ahora entender esta asignación, que agrega un nuevo elemento al final de la lista:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista[len(lista):] = ['F']
>>> lista
['a', 'b', 'c', 'd', 'e', 'F']

Esto es equivalente a ejecutar el método append:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista.append('F')
>>> lista
['a', 'b', 'c', 'd', 'e', 'F']

Curiosamente, cualquier valor mayor o igual que len(lista) generaría el mismo resultado:

>>> lista[100:] = ['F']
>>> lista
['a', 'b', 'c', 'd', 'e', 'F']

Podemos extender, por supuesto, la lista con más elementos de una sola vez, no sólo de uno en uno:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista[len(lista):] = ['F', 'G']
>>> lista
['a', 'b', 'c', 'd', 'e', 'F', 'G']

El método extend realiza exactamente lo mismo:

>>> lista = ['a', 'b', 'c', 'd', 'e']
>>> lista.extend(['F', 'G'])
>>> lista
['a', 'b', 'c', 'd', 'e', 'F', 'G']

Hemos aplicado al slicing de listas en Python su misma medicina y lo hemos dejado bien troceado en conceptos simples, pero poderosos. Utilízalos a discreción y, ante todo, cuida tus dedos

Javier Montero Gabarró


http://elclubdelautodidacta.es/wp/2015/12/python-troceando-desde-el-lado-izquierdo/


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Safe Creative #1512166040787

LaTeX – Control de la numeración de las listas

Objetivo: aprender a controlar la numeración de las listas numeradas en LaTeX.

En los primeros compases de la serie, al presentar los rudimentos de \LaTeX, recorrimos los diversos tipos de listas básicos, entre las que se encontraban las listas numeradas, caracterizadas por que cada elemento aparece precedido por un número de orden. Continuaremos el artículo mostrando algunas técnicas que nos proporcionarán un mayor control de la numeración.

Para crear una lista numerada en \LaTeX, como vimos, disponemos del entorno enumerate, indicando cada elemento de la lista mediante el comando \item, como en este ejemplo:

\begin{enumerate}
\item Pepino
\item Berenjena
\item Pimiento
\end{enumerate}

La técnica básica es simple, no tiene mayor misterio. Como resultado obtenemos una lista en la que cada elemento aparece numerado, comenzando, como es de esperar, por el uno y prosiguiendo consecutivamente.

Este esquema es lo suficientemente inteligente para adaptarse y renumerar el conjunto adecuadamente si introducimos nuevos elementos a la lista en cualquier posición. Pero, ¿cómo podemos hacer para que la lista comience su numeración por el valor que deseemos, no necesariamente por el uno?

Para controlar el valor inicial de multitud de estructuras numéricas, \LaTeX dispone del concepto de contador. En concreto, el contador que gestiona la numeración de las listas numeradas se denomina enumi. Si queremos que nuestra lista de vegetales se muestre a partir del número 5, por ejemplo, debemos incluir el siguiente comando dentro del entorno enumerate, antes de cualquier descripción de elemento:

Lista 1
\begin{enumerate}
\setcounter{enumi}{4}
\item Pepino
\item Berenjena
\item Pimiento
\end{enumerate}

latex-ln1

El valor que se establece como contador es el entero inmediatamente anterior al que deseamos que sirva de inicio. Observa que he dicho entero, y no natural. En efecto, este contador puede ser negativo también. Si, por ejemplo, indicáramos un -3, el primer elemento de la lista sería -2, seguido de -1, 0, 1, etc. En particular, si quieres que la lista comience por el cero, el valor de enumi será -1.

Presta atención también la sintaxis de setcounter, que requiere dos argumentos obligatorios (entre llaves, no en corchetes): el nombre del contador seguido del valor numérico.

Es importante que setcounter se introduzca dentro del bloque enumerate, pues si estuviera fuera se restablecería automáticamente a su valor por defecto al encontrarse con el begin de apertura del entorno. Además, debe figurar antes que los elementos; de lo contrario, la renumeración sólo aparecería desde su posición en adelante. Prueba tú mismo lo que sucedería si situaras setcounter entre medias de la lista.

Otra cuestión que se plantea a menudo es cómo gestionar la numeración de varias listas, generadas cada una por sus respectivos entornos enumerate, cuando deseamos que las sucesivas continúen a partir donde finalizaron las anteriores.

Naturalmente, siempre podemos resolverlo a mano: contamos los elementos existentes en cada lista y ajustamos el contador apropiadamente. Pero eso presenta el inconveniente de que, si añadimos más elementos a las listas habremos de reajustar los contadores para que reflejen el cambio.

Existen varios modos de obtener una numeración automática. La más simple consiste en recurrir a un paquete que se ocupe de ese trasiego, como enumitem, un excelente trabajo de Javier Bezos.

\usepackage{enumitem}

A partir de ese momento, dispondremos de un argumento opcional para el entorno enumerate, resume, que se ocupará de retomar la numeración justo en el punto donde finalizó la anterior.

Lista 1 
\begin{enumerate}
\item Pepino
\item Berenjena
\item Pimiento
\end{enumerate}

Lista 2
\begin{enumerate}[resume]
\item Calabacín
\item Zanahoria
\item Remolacha
\end{enumerate}

latex-ln3

El paquete enumiten hace mucho más que eso, desde luego; consulta su documentación si sientes curiosidad. Presiento que volveremos a él en alguna que otra ocasión.

Las posibilidades de \LaTeX, virtualmente ilimitadas a través de sus innumerables paquetes, hacen que la única manera de no perderse en ellas sea manteniendo una especie de diario personal donde anotemos aquellas técnicas que nos resulten de utilidad práctica. No es una labor en absoluto enciclopédica: registra únicamente las que verdaderamente puedas necesitar. Y, por supuesto, no debiera hacer falta decirlo, escrito en \LaTeX. ¿En qué, si no?

Javier Montero Gabarró


http://elclubdelautodidacta.es/wp/2015/10/latex-control-de-la-numeracion-de-las-listas/


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Índice completo de artículos relacionados con \LaTeX.

Safe Creative #1510245609313

Python – Un poco de orden, por favor

Objetivo: mostrar cómo ordenar los elementos de una lista.

Benditas sean las listas. ¡Qué sería de nosotros, pythonistas, sin su admirable flexibilidad! He perdido la cuenta de los problemas que he podido resolver con elegancia apoyándome simplemente en estas sólidas estructuras.

Vamos a ampliar el repertorio de las posibilidades que nos ofrecen las listas. Nos ocuparemos hoy de ordenarlas.

Tomemos, por ejemplo, la siguiente lista numérica:

>>> a = [7, -1, 5, 3]

Ordenarla resulta tan sencillo como aplicar el método sort(), disponible en los objetos de tipo lista:

>>> a.sort()
>>> a
[-1, 3, 5, 7]

Bien simple. No obstante hay algo que debes tener siempre presente: esta modificación es “in situ”, atacando directamente al objeto original, sin crear una copia.

Si lo que quieres es generar una nueva lista ordenada, pero sin afectar la original, puedes hacer uso de la función sorted(), que toma como argumento una secuencia y devuelve otra diferente ordenada.

>>> a = [7, -1, 5, 3]
>>> b = sorted(a)
>>> a
[7, -1, 5, 3]
>>> b
[-1, 3, 5, 7]

Mucho cuidado con hacer esto:

>>> a = [7, -1, 5, 3]
>>> b = a.sort()

Naturalmente, a se modificaría, ordenándose. ¿Pero qué crees que valdría b?

>>> print(b)
None

Nada. b no vale nada.

Si no entiendes por qué sucede esto, no te pierdas la lectura del artículo Python – Hace falta valor, en el que se desentraña el misterio.

Lo bueno de la función sorted() es que el argumento puede ser una secuencia en general, no solamente una lista. Así, objetos no mutables, como las tuplas, que no disponen del método sort(), podrían beneficiarse de su utilización.

La única limitación de sort() y sorted() es que los elementos han de ser comparables. Si no, Python difícilmente podrá deducir su orden.

La siguiente lista, con tipos diferentes, no podrá ser ordenada por sort():

>>> listamixta = [1, 'a', 5, 'casa']
>>> listamixta.sort()
Traceback (most recent call last):
  File "<pyshell#83>", line 1, in <module>
    listamixta.sort()
TypeError: unorderable types: str() < int()

Ordenar una lista en sentido inverso, de mayor a menor, es igualmente fácil haciendo uso de un argumento opcional, reverse:

>>> a = [7, -1, 5, 3]
>>> a.sort(reverse = True)
>>> a
[7, 5, 3, -1]

La función sorted() dispone también de la misma posibilidad:

>>> a = [7, -1, 5, 3]
>>> sorted(a, reverse = True)
[7, 5, 3, -1]

Observa que, en este último ejemplo, a no ha modificado su valor, puesto que sorted() ha creado un objeto diferente:

>>> a
[7, -1, 5, 3]

Voy a plantearte ahora un problema interesante que servirá de preludio al argumento que presentaremos a continuación y que aumentará sobremanera la potencia de nuestras ordenaciones.

Imagina que queremos ordenar alfabéticamente la siguiente lista:

>>> frutas = ['pera', 'Manzana', 'fresa']

Observa que he comenzado en mayúsculas la palabra Manzana.

El método sort(), estrictamente, cumple su función:

>>> frutas.sort()
>>> frutas
['Manzana', 'fresa', 'pera']

Las letras mayúsculas se almacenan internamente con un código más bajo que las correspondientes minúsculas, de modo que Manzana aparece antes que fresa, pese a que tal vez no fuera eso lo que nos gustaría.

¿Cómo hacer para que la ordenación no tenga en cuenta que la M está en mayúsculas y la trate como si fuera minúscula, pero dejando que aparezca en el resultado tal como fue escrita?

Imagina que podemos crear un tratamiento temporal previo que procese cada término conviertiéndolo completamente en minúsculas para que luego sort trabaje sobre ese resultado, pero sin olvidar cuáles eran los términos originales.

Esto se logra con un nuevo argumento, key:

>>> frutas = ['pera', 'Manzana', 'fresa']
>>> frutas.sort(key = str.lower)
>>> frutas
['fresa', 'Manzana', 'pera']

Y ahora sí, fresa aparece antes que Manzana.

Presta atención: key recibe como valor el nombre de una función que requiera un único argumento. El valor devuelto por la función será utilizado después como base de trabajo para la ordenación.

El método lower(), que se aplica a los objetos de tipo str, strings, convierte una cadena de caracteres toda en minúsculas. Observa que he dicho el nombre de una función, por eso lower se muestra sin paréntesis en el argumento key.

Otro ejemplo. Reorganicemos nuestra macedonia, pero esta vez de modo que las frutas aparezcan ordenadas de acuerdo a su longitud:

>>> frutas = ['pera', 'Manzana', 'fresa']
>>> frutas.sort(key = len)
>>> frutas
['pera', 'fresa', 'Manzana']

Como sabes, la función len devuelve el número de elementos de una secuencia, esto es, el número de letras de que se compone un string, en nuestro caso. Ese total será tomado como criterio para la ordenación.

Podemos utilizar key para saltarnos la limitación que nos impedía ordenar listas mixtas.

>>> listamixta = [1, 'a', 5, 'casa']
>>> listamixta.sort(key = str)
>>> listamixta
[1, 5, 'a', 'casa']

La función str convierte un objeto en string, de modo que los números ya serán comparables con el resto de valores y sort() podrá realizar su trabajo. Observa que la lista resultado sigue siendo mixta; la conversión a string sólo se ha realizado a nivel interno.

Podemos ir más allá y crear incluso nuestras propias funciones para utilizarlas en sort().

Por ejemplo, esta sencilla función invierte un string:

>>> def invertir(cadena):
  return cadena[::-1]

>>> invertir('pimiento')
'otneimip'

Si no entiendes cómo trabaja esta función, echa un vistazo al artículo El mundo al revés.

Vamos a aprovecharla entonces como criterio de ordenación para que sort() ordene la lista atendiendo a la última letra de cada palabra, en lugar de la primera, tal como haría por omisión.

>>> planetas = ['mercurio', 'venus', 'tierra', 'marte']
>>> planetas.sort(key = invertir)
>>> planetas
['tierra', 'marte', 'mercurio', 'venus']

Fíjate por dónde, nuestro planeta el primero…

La función sorted(), como cabría esperar, dispone también del argumento key.

Interesante. Las posibilidades creativas son inmensas. Prácticamente significa que puedes hacer que Python ordene una lista o, por lo general, cualquier secuencia, por cualquier criterio que puedas imaginar. Es lo que me encanta de Python: este lenguaje rezuma creatividad lo mires por donde lo mires.

Javier Montero Gabarró


Python – Un poco de orden, por favor


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Python – Eliminación de múltiples elementos repetidos de una lista

Objetivo: presentar algunas técnicas en Python para la eliminación de elementos que aparecen repetidos en una lista.

La necesidad de eliminar elementos en una lista es algo común en cualquier proyecto de programación. Repasaremos, en primer lugar, la eliminación de un elemento simple empleando los recursos propios del objeto list para luego explicar cómo proceder en el caso de que queramos suprimir elementos repetidos.

Partamos del siguiente problema simple: dada una lista queremos eliminar cierto elemento dado. Por ejemplo, supongamos que nos ofrecen un gazpacho preparado del siguiente modo:

>>> gazpacho = ['aceite', 'vinagre', 'leche', 'tomate', 'pan', 'pepino', 'pimiento']

Sobre gustos no hay nada escrito, pero yo, desde luego, me cuidaría de probar semejante combinación. Dudo mucho que la leche haga buenas migas con los restantes ingredientes.

Aprovechemos nuestros conocimientos sobre Python para devolver esa pócima imbebible a su estado natural. Como ya sabemos por artículos anteriores, suprimir la leche es tan simple como esto:

>>> gazpacho.remove('leche')
>>> gazpacho
['aceite', 'vinagre', 'tomate', 'pan', 'pepino', 'pimiento']

Y ahora sí que me tomaría siete vasos seguidos.

El método remove modifica la lista in-situ, atacando al objeto original y suprimiento directamente en él el elemento indicado. Comprobamos que efectivamente la eliminación ha sido in-situ constatando que la invocación del método remove no ha devuelto ningún valor en el intérprete (si no sabes de qué estoy hablando, te aconsejo encarecidamente que te leas el artículo Hace falta valor, que te ayudará a comprender lo que está sucediendo entre bastidores).

El método remove tiene una limitación: sólo elimina la primera aparición del elemento en la lista. Pero, ¿y si queremos que desaparezca completamente si está repetido más veces?

Existen muchas formas de resolver esto; yo voy a mostrarte dos de las más elegantes.

La primera es casi obvia: descomponiendo el problema en otros más simples. La eliminación de muchos elementos no es más que la eliminación de uno solo unas cuantas veces.

Supongamos la siguiente colección de cine infantil:

cine = ['Bambi', 'Blancanieves', 'Emmanuelle', 'El rey león', 'Emmanuelle']

Parece que se nos ha colado alguna cinta ajena a la temática. El siguiente bucle las elimina de una en una aplicando el método remove:

>>> while 'Emmanuelle' in cine:
  cine.remove('Emmanuelle')

  
>>> cine
['Bambi', 'Blancanieves', 'El rey león']

Hemos hecho uso del operador in para comprobar la existencia del elemento en la lista.

Pero mi método favorito es recurriendo a la elegancia de las listas por comprensión.

Reconstruyamos nuestra colección original primero, modificada inapelablemente tras cada remove.

cine = ['Bambi', 'Blancanieves', 'Emmanuelle', 'El rey león', 'Emmanuelle']

Recordemos las distintas fases:

1) recorremos el iterable:

for peli in cine

2) escribimos a la izquierda la transformación deseada. No queremos transformar nada, sólo dejar cada película tal y como está:

peli for peli in cine

3) a la derecha escribimos la condición de filtrado:

peli for peli in cine if peli != 'Emmanuelle'

Estamos diciendo que sólo se seleccionen aquellos elementos distintos a ‘Emmanuelle’.

4) encerramos el conjunto entre llaves y ¡listo!:

>>> [peli for peli in cine if peli != 'Emmanuelle']
['Bambi', 'Blancanieves', 'El rey león']

Elegante. Acostúmbrate a usar las listas por comprensión, representan potencia sutil con la que podrás resolver numerosos problemas.

Aquí no es solo una cuestión de elegancia. Si te fijas, esta vez la invocación ha provocado que el intérprete devolviera un valor. Eso significa que ha sido creado un nuevo objeto.

En efecto, la lista original no ha sido afectada, como podemos comprobar:

>>> cine
['Bambi', 'Blancanieves', 'Emmanuelle', 'El rey león', 'Emmanuelle']

Lo que te permite recuperar Emmanuelle, todo un clásico del cine erótico digno de cualquier filmoteca que se precie.

Pero, si tu intención era esa, podrías haber procedido directamente del siguiente modo:

>>> cine = [peli for peli in cine if peli != 'Emmanuelle']
>>> cine
['Bambi', 'Blancanieves', 'El rey león']

Forzando así que la variable cine referencie ahora al nuevo objeto creado en lugar de al anterior, evitándote así oscuras tentaciones.

Javier Montero Gabarró


Python – Eliminación de múltiples elementos repetidos de una lista


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Python – Listas por comprensión – 1

Objetivo: introducir formalmente una técnica poderosa para la creación y modificación de listas: las listas por comprensión.

En un artículo reciente presentamos varios modos de abordar la necesidad de modificar una lista iterando sobre ella.

Échale un vistazo para situarte en contexto si no lo recuerdas; te llevará apenas un minuto.

Tras presentar las técnicas de modificación con la creación de una nueva lista temporal y mediante el acceso a través del índice, soltamos, a modo de aperitivo, un bombazo en perfecto estilo pythonesco:

>>> lista = [1, 2, 3, 4, 5]
>>> lista = [elemento * elemento for elemento in lista]
>>> lista
[1, 4, 9, 16, 25]

Nos ha bastado una única instrucción para transformar la lista por otra con sus elementos elevados al cuadrado.

Las listas por comprensión no sólo suponen un modo más elegante y rápido de escribir código, sino que además se ejecutan más rápidamente también, al estar implementadas directamente en lenguaje C.

Supongamos que tenemos una lista compuesta de cadenas de caracteres que queremos transformar en mayúsculas:

>>> alioli = ['ajo', 'aceite']

Para generar la lista por comprensión procedemos del siguiente modo:

1) Escribimos una iteración que recorra la lista:

for s in alioli

2) Agregamos a la izquierda lo que queremos que suceda con cada elemento:

s.upper() for s in alioli

3) Finalmente, rodeamos toda la expresión entre corchetes:

[s.upper() for s in alioli]

Observémoslo en acción:

>>> [s.upper() for s in alioli]
['AJO', 'ACEITE']

Es muy importante comprender que este procedimiento no modifica la lista original, que mantiene su valor.

>>> alioli
['ajo', 'aceite']

Se ha creado, por el contrario, un nuevo objeto lista que podría ser referenciado por cualquier otra variable.

Por supuesto, si lo que deseábamos era transformar la lista original podríamos haber hecho directamente la siguiente asignación:

>>> alioli = [s.upper() for s in alioli]

Ahora sí: alioli está referenciando la nueva lista por comprensión generada en lugar de la antigua:

>>> alioli
['AJO', 'ACEITE']

Una señora salsa en mayúsculas…

La iteración podemos realizarla sobre cualquier estructura susceptible de ser iterada, no necesariamente sobre una lista.

El siguiente ejemplo toma una cadena de caracteres y construye una lista con el código de cada carácter individual de la cadena:

>>> s = 'Me tomaría ahora una buena ración de papas al alioli'
>>> codigos = [ord(caracter) for caracter in s]
>>> codigos
[77, 101, 32, 116, 111, 109, 97, 114, 237, 97, 32, 97, 104, 111, 
114, 97, 32, 117, 110, 97, 32, 98, 117, 101, 110, 97, 32, 114, 
97, 99, 105, 243, 110, 32, 100, 101, 32, 112, 97, 112, 97, 115, 
32, 97, 108, 32, 97, 108, 105, 111, 108, 105]

Como ejercicio te propongo que realices la misma operación empleando la metodología tradicional.

Me ha entrado mucha hambre escribiendo esto, de modo que continuaremos hablando otro día de las listas por comprensión y presentaremos una nueva sintaxis ampliada que las hará más potentes aún.

Javier Montero Gabarró


Python – Listas por comprensión – 1


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Python: Una función para iterar sobre listas anidadas

Objetivo: crear una función que ilustre cómo iterar sobre listas anidadas de cualquier profundidad.

Un lector del blog me planteaba ayer una interesante cuestión al hilo del artículo dedicado a la iteración sobre una lista con la sentencia for. Se preguntaba cómo proceder en el supuesto de que se tratara de listas anidadas en las que no conocemos a priori su profundidad de anidación.

Por ejemplo, supongamos que deseamos imprimir todos los enteros existentes en la siguiente lista:

lista_anidada = [1, [2, [3, 4, [5, 6], 7]], 8, [9, 10]]

Observa que el segundo elemento, que es el que presenta mayor nivel de anidación, es en realidad otra lista, cuyo segundo elemento es a su vez otra lista de la cual el tercer elemento es otra lista también. En este ejemplo en particular nos encontramos hasta cuatro niveles de profundidad. Nuestra función deberá ser genérica, en el sentido de que no conocerá de antemano la complejidad de la lista.

Una iteración ordinaria nos devolvería el siguiente resultado:

for elemento in lista_anidada:
    print(elemento)

1
[2, [3, 4, [5, 6], 7]]
8
[9, 10]

No nos sirve, pues pretendemos recuperar todos los elementos simples.

Procederemos de la siguiente forma:

Vamos a recorrer la lista de principio a fin. Si el elemento a tratar es una lista habrá que imprimir sus elementos individuales; si no lo es, imprimimos directamente su valor.

Observa con cuidado la frase “si el elemento a tratar es una lista habrá que imprimir sus elementos individuales”. Es exactamente el mismo problema que al inicio, salvo que está reducido a un subconjunto más reducido: la lista anidada dentro de la lista.

Al resolver esta segunda cuestión nos aparecería nuevamente una lista anidada, esta vez del tercer nivel; el problema seguiría siendo exactamente el mismo, pero cada vez más reducido.

Escenario perfecto para una función recursiva

Para resolver la parte que dice “si el elemento a tratar es una lista”, recurriremos a una función de Python que nos permite saber si un objeto es o no de un tipo determinado: isinstance().

Obsérvala en acción:

>>> a = 5
>>> b = 'casa'
>>> c = [1, 2]
>>> isinstance(a, list)
False
>>> isinstance(b, list)
False
>>> isinstance(c, list)
True

Implementar nuestra función resulta ya una tarea casi obvia:

def imprimir(lista):
    for elemento in lista:
        if isinstance(elemento, list):
            imprimir(elemento)
        else:
            print(elemento)

Fíjate en la técnica recursiva: si elemento es una lista, la función vuelve a llamarse a sí misma, pero esta vez sobre una lista anidada en el siguiente nivel de profundidad. Y así sucesivamente, ahondando todo lo necesario hasta que no quede un entero sin imprimir.

def imprimir(lista):
    for elemento in lista:
        if isinstance(elemento, list):
            imprimir(elemento)
        else:
            print(elemento)

lista_anidada = [1, [2, [3, 4, [5, 6], 7]], 8, [9, 10]]

imprimir(lista_anidada)

>>> 
1
2
3
4
5
6
7
8
9
10

No se salva ni el apuntador.

Javier Montero Gabarró


http://elclubdelautodidacta.es/wp/2013/02/python-una-funcion-para-iterar-sobre-listas-anidadas/


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Python: Iterando y modificando una lista

Objetivo: presentar técnicas de iteración sobre listas que permitan su modificación in-situ.

Comencemos planteando el siguiente problema: tenemos una lista de números y queremos modificarla de modo que almacene, en lugar del número en sí, su cuadrado.

No parece un problema complejo que no sepamos resolver con lo que ya conocemos:

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

for elemento in lista:
    elemento = elemento * elemento
    print(elemento) # Una simple verificación

Si ahora observamos la salida:

>>> 
1
4
9
16
25

Aparenta todo haber ido de maravilla; sin embargo, nos encontramos con un pequeño detalle importante:

>>> lista
[1, 2, 3, 4, 5]

¡La lista no ha cambiado!

¿Cómo es posible? ¿Acaso no hemos comprobado como elemento, que ha pasado sucesivamente por todos y cada uno de los elementos de la lista, ha actualizado su valor a elemento * elemento?

Así es. Pero, lo que sucede, es que elemento no es más que una copia del valor original. Por muchos cambios que efectuemos sobre él no afectaran a la lista.

Necesitamos cambiar de estrategia.

Una forma de salvar el escollo sería reconstruir una lista temporal con los nuevos valores al cuadrado.

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

lista_temp = []

for elemento in lista:
    elemento = elemento * elemento
    print(elemento)
    lista_temp.append(elemento)

La lista temporal, lista_temp es, en principio, una lista en blanco. Con cada iteración le agregamos el nuevo valor de elemento mediante el método append().

lista_temp ya tiene la solución buscada:

>>> lista
[1, 2, 3, 4, 5]
>>> lista_temp
[1, 4, 9, 16, 25]

Si lo necesitamos, podemos reasignar hacia dónde apunta lista:

>>> lista = lista_temp
>>> lista
[1, 4, 9, 16, 25]

En un nivel mayor de elegancia (en Python, la elegancia suele coincidir con el menor número de líneas de código), está la siguiente técnica: recorriendo el bucle for a través de el índice de la lista en vez de sus elementos. Es decir, como si fuera un bucle for típico de cualquier otro lenguaje de programación:

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

for indice in range(len(lista)):
    lista[indice] = lista[indice] * lista[indice]
    print(lista[indice])

El resultado es el que esperamos:

>>> 
1
4
9
16
25
>>> lista
[1, 4, 9, 16, 25]

No solo la impresión es correcta, sino que la lista ha sido actualizada in-situ. Desde el bucle for, a través del índice, hemos podido acceder directamente a la lista, no a una copia de ella.

Apréndete bien esta técnica, pues es muy común. Itera sobre los elementos o los índices según tu necesidad concreta.

Date cuenta de que, para poder iterar a través de los índices, necesitamos conocer el número de elementos totales de la lista. Esto lo obtenemos con la expresión len(lista). Recuerda que la función len() nos devuelve el total de elementos de una secuencia. A continuación la función range() nos construye un iterable: range(len(lista)) facilitará al bucle for, sucesivamente, todos los valores enteros desde cero hasta uno menos que la longitud de la lista, precisamente el mismo rango que tienen sus índices.

Existe un nivel mayor de elegancia aún, las listas por comprensión, una de las construcciones más fascinantes y poderosas de Python. Son merecedoras de uno o varios artículos por separado, pero no me gustaría despedirme hoy sin que, al menos, y a modo de aperitivo, compruebes su potencia:

lista = [1, 2, 3, 4, 5]
lista = [elemento * elemento for elemento in lista]

Así de elegante:

>>> lista
[1, 4, 9, 16, 25]

Bienvenido a Python.

Javier Montero Gabarró


Python: Iterando y modificando una lista


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Python: El mundo al revés

Objetivo: aprender a invertir secuencias en Python.

¿Cansado de que tu nombre figure siempre en las últimas páginas de las listas de resultados de exámenes? ¿No has podido renovar el DNI después de varias horas esperando en una cola? Si es así, necesitas leer este artículo cuanto antes.

Voy a mostrarte cómo invertir secuencias en Python, ponerlas patas arriba y presentarlas al revés.

Comencemos considerando una cadena de caracteres cualquiera:

>>> a = 'pradera'

La técnica para invertir secuencias se basa en los slices, que ya deben resultarte familiares si has leído los artículos anteriores.

Repasemos su sintaxis:

secuencia[i:j:k]

El primer parámetro indica el índice de comienzo del trozo y el segundo el final, pero sin incluir. El tercero es opcional y, cuando está presente, indica el incremento con el que recorreremos el camino entre el principio y el fin.

Veamos algunos ejemplos:

>>> a[0:3]
'pra'

Hemos cortado un fragmento que abarca desde el carácter que tiene por índice 0 (es decir, el primero), hasta el que tiene 3 sin incluir (esto es, hasta el índice 2).

El siguiente slice hace uso de los tres parámetros:

>>> a[1:6:2]
'rdr'

Para comprenderlo, extrae primero el fragmento que correspondería a [1:6]:

'rader'

Y ahora recorre la secuencia de dos en dos, empezando por el primero:

'rdr'

El siguiente ejemplo muestra la omisión del primer parámetro, indicando que comenzamos desde el principio:

>>> a[:4]
'prad'

O del segundo, lo que significa que sigue hasta el final:

>>> a[3:]
'dera'

O de ambos:

>>> a[:]
'pradera'

Recuerda que ya presentamos esta última técnica como método para duplicar una secuencia.

Visto este pequeño repaso, lo que nos interesa muy particularmente es el caso en el que el tercer parámetro del slice es negativo. Eso significa que el fragmento se extrae no de izquierda a derecha, sino al revés, de derecha a izquierda.

Observa el ejemplo:

>>> a[5:1:-1]
'reda'

Analicemos esto con cuidado esto para asegurarnos de que lo entendemos bien:

Lo primero que quizás te llame la atención es que el primer parámetro es mayor que el segundo. Esto es así debido a que estamos recorriendo la secuencia de derecha a izquierda. El índice de comienzo siempre será más alto (o al menos igual) que el índice de destino.

Comenzamos el corte hacia atrás desde el carácter que tiene por índice 5: ‘r’.

¿Y dónde terminamos? En el inmediatamente anterior al indicado en el segundo parámetro. Puesto que estamos recorriendo la secuencia al revés, el anterior del índice 1 no es el índice cero sino 2: ‘a’.

Y ahora sí, tomamos de derecha a izquierda todos esos caracteres, desde un extremo hasta el otro:

'reda'

Ya estás en condiciones de entender lo que hace esto:

>>> a[::-1]
'aredarp'

Es nuestro buscado string invertido.

Como no fijamos valores iniciales ni finales estamos indicando que actuaremos sobre la cadena completa.

Apréndete bien esta técnica, es el procedimiento básico para invertir secuencias, algo que tendrás que realizar con frecuencia en tu vida como programador.

Naturalmente, con las listas (un tipo de secuencias) otro tanto de lo mismo:

>>> a = [1, 2, 3]
>>> a[::-1]
[3, 2, 1]

Es muy importante que tengas siempre presente que el slicing siempre genera un objeto nuevo. Con los strings puede parecer obvio, ya que son inmutables, pero quizás no tanto con las listas, que no lo son (pueden modificarse in-situ).

Constata que la lista sigue siendo la misma:

>>> a
[1, 2, 3]

Por último, voy a enseñarte otra técnica muy útil para invertir listas, pero con la particularidad de que realizaremos una operación destructiva, modificando in-situ la lista (aprovechándonos de su mutabilidad): el método reverse().

>>> a.reverse()
>>> a
[3, 2, 1]

Observa que no ha habido creación de un duplicado, sino que el objeto original ha sido directamente modificado.

La siguiente lista muestra el resultado de una carrera entre varios colegas:

>>> clasificacion = ['Jesús', 'Pedro', 'José Luis', 'Javier']

No es que me importe particularmente quedarme el último pero, si dispongo de recursos, ¿por qué no los habría de utilizar?

>>> clasificacion.reverse()
>>> clasificacion
['Javier', 'José Luis', 'Pedro', 'Jesús']

Ni slicing ni gaitas: he preferido actuar directamente sobre el objeto, destruyendo el anterior.

Y ahora eso ya tiene otra pinta, ¿somos pythonistas o no?

Javier Montero Gabarró


Python: El mundo al revés


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Python: Cómo copiar una lista

Objetivo: aplicar las técnicas de slicing de secuencias para obtener un duplicado de una lista.

En el artículo anterior, El sagrado misterio de la inmutabilidad, pudimos apreciar un fenómeno curioso a la hora de trabajar con listas.

Supongamos las siguientes asignaciones aparentemente inocentes:

>>> x = [1, 2]
>>> y = x

Tiene toda la pinta de que y es una copia de x, ¿no?.

>>> y
[1, 2]

Si ahora modificamos la lista x:

>>> x.append(3)
>>> x
[1, 2, 3]

¿Qué habrá sucedido con y?

>>> y
[1, 2, 3]

Atención: ¡los cambios en x también han ocurrido en y! Esto es Python.

Lo que está pasando es que x e y se corresponden con el mismo objeto en memoria. Esto no tendría la menor importancia desde el punto de vista del programador si el objeto en cuestión fuera inmutable, como un número, un string o una tupla, pues no pueden ser modificados in situ. En cambio, con las listas tenemos un serio problema, como hemos podido comprobar.

¿Cómo hacemos, entonces, si necesitamos duplicar una lista de modo que cada una pueda vivir su vida libre independientemente de la otra?

Las técnicas de slicing de secuencias, que ya hemos dejado caer con anterioridad, permiten la independencia de las listas clonadas.

Repasemos brevemente el concepto de slicing, o troceado de una secuencia.

>>> x = 'pradera'
>>> x[1:5]
'rade'

Con esa instrucción extraemos desde el elemento cón índice 1 (recuerda que, en las secuencias, el primer elemento tiene por índice cero) hasta, y sin incluir, el de índice 5; es decir, desde el 1 al 4.

Fíjate en el formato de la instrucción: entre corchetes y separando los límites mediante los dos puntos.

Podemos omitir uno de los índices:

>>> x[:5]
'prade'

que equivale a tomar todos los caracteres desde el principio hasta el de índice cuatro.

>>> x[3:]
'dera'

que es lo mismo que extraer desde el que tiene por índice 3 hasta el último de la secuencia.

O también podemos suprimir los dos, equivalente a extraer todo desde el principio hasta el fin:

>>> x[:]
'pradera'

¿Qué sentido tiene realizar algo así, te preguntarás?

La clave estriba en que siempre que hagamos un slicing de una lista, obtendremos un objeto distinto, aunque sea un slicing como el anterior, de principio a fin.

Obsérvalo:

>>> x = [1, 2]
>>> y = x[:]
>>> y
[1, 2]

Tras esta operación y contiene el resultado de la extracción completa de los elementos de x, pero ahora sí que se trata de un duplicado independiente. Comprobémoslo:

>>> id(x)
162280364
>>> id(y)
162280556

También podríamos haber empleado el operador is para verificar si dos variables apuntan al mismo objeto, lo que es más rápido que comprobar las identidades individuales:

>>> x is y
False

Hemos logrado separar a las listas siamesas. Una última comprobación:

>>> x.append(3)
>>> x
[1, 2, 3]
>>> y
[1, 2]

Ley de vida: x e y dejan de ser almas paralelas y cada una recorre ahora su propio camino hasta que, inevitablemente, llegue el día en que dejen de existir.

Javier Montero Gabarró


http://elclubdelautodidacta.es/wp/2012/09/python-como-copiar-una-lista/


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Python – Capítulo 29: Tuplas

Ha llegado el momento de que hablemos de una estructura de datos muy similar a las ya conocidas listas: las tuplas.

Al igual que una lista, una tupla es una secuencia ordenada de objetos. La principal diferencia entre ambas es que la primera puede ser modificada mientras que una tupla es inmutable: una vez creada, su contenido no puede modificarse (pero sí sobreescribirse, lo que equivaldría a volver a crear una tupla con el mismo nombre). Recuerda los métodos append(), extend(), remove(), pop(), etc., que, de un modo u otro, afectaban al contenido de una lista. Estos métodos no están disponibles en las tuplas.

Piensa en una tupla como una estructura más ágil que una lista, descargada de parafernalia que no siempre es necesaria. En ocasiones pueden incluso ayudarte a escribir código menos propenso a errores, en el supuesto de que, por accidente, intentes modificar una secuencia de objetos que quieres proteger.

La forma básica de definir una tupla es facilitando una secuencia simple de objetos separados entre comas:

>>> frutas='melon', 'sandía'
>>> frutas
('melon', 'sandía')

Date cuenta que no necesitamos corchetes ni paréntesis de ningún tipo. Sin embargo, al visualizarla, nos aparece con paréntesis.

Llegamos con esto a un aspecto importante: el uso de paréntesis es opcional. Podrías haber creado la tupla encerrando los datos entre paréntesis del mismo modo:

>>> frutas=('melon', 'sandía')
>>> frutas
('melon', 'sandía')

Recuerda que las listas, por el contrario, van encerradas entre corchetes.

Los paréntesis no son necesarios, pero hay ocasiones en las que su uso es obligatorio cuando su falta provoca situaciones ambiguas. Por ejemplo, observa la llamada a esta función:

funcioncilla(1,2,3)

¿Es una función con tres argumentos enteros o uno sólo en forma de tupla? La respuesta es, obviamente, la primera. Si hubiera querido que el argumento fuera una tupla debería haberlo indicado entre paréntesis para solucionar la ambigüedad:

funcioncilla((1,2,3))

Python te ofrece flexibilidad a la hora de representar una tupla. Hay programadores que sólo utilizan los paréntesis cuando no queda más remedio; otros, en cambio, los indican siempre. Hay también un término medio que emplea una notación mixta en función de su propio estilo. Elige el que mejor te plazca.

Al igual que una lista, una tupla puede contener una mezcla de objetos de distintos tipos:

>>> cajon='lunes',5,[2,3]
>>> cajon
('lunes', 5, [2, 3])

El primer elemento es una cadena de caracteres, el segundo un entero y el tercero una lista.

Puedes consultar los elementos de una tupla accediendo a través de su índice:

>>> cajon[1]
5

Recuerda que el primer elemento tiene por índice cero, como en las listas.

Pero observa qué sucede si tratas de modificar la tupla:

>>> cajon[1]=7
Traceback (most recent call last):
  File "<pyshell#8>", line 1, in <module>
    cajon[1]=7
TypeError: 'tuple' object does not support item assignment 

Sencillamente, no te deja.

Debes saber algo más a la hora de crear tuplas. Para crear una tupla vacía, sin elementos, abrimos y cerramos paréntesis sin más:

>>> vacia=()
>>> vacia
()

Si no te crees que esto es una tupla, prueba con la función type() para saber de qué objeto estamos hablando:

>>> type(vacia)
<class 'tuple'>

Para crear una tupla con un único elemento, debemos finalizar la enumeración con una coma:

>>> solo_uno=1,
>>> solo_uno
(1,)
>>> type(solo_uno)
<class 'tuple'>

Naturalmente, esto se podía hecho también con paréntesis:

>>> solo_uno=(1,)

Habría sido un error, si lo que pretendíamos era generar una tupla, escribir lo siguiente:

>>> solo_uno=(1)
>>> type(solo_uno)
<class 'int'>

Date cuenta de que type() nos está devolviendo un entero en vez de una tupla.

En resumidas cuentas, a no ser que crees una tupla vacía, asegúrate de que, al menos, exista una coma, emplees o no paréntesis.

Muchas veces se dice de las tuplas que no disponen de métodos, al ser inmutables. Esto no es cierto: no existen métodos para tuplas que modifiquen su contenido, pero sí otros que no las alteren. Por ejemplo:

>>> cajon.count('lunes')
1

El método count() nos devuelve cuántos elementos existen con una valor determinado.

O también:

>>> cajon.index('lunes')
0

El método index() nos devuelve el índice de la primera ocurriencia que haya del valor buscado.

Al igual que las listas, las tuplas pueden ser troceadas:

>>> cajon[1:]
(5, [2, 3])

Esta instrucción ha generado una nueva tupla tomando los elementos que tienen por índice 1 y sucesivos de la tupla cajon.

Existen muchos usos curiosos de las tuplas que quedan camuflados por el hecho de que pueden aparecer sin paréntesis. Iremos conociéndolos, paciencia…

Javier Montero Gabarró


Python – Capítulo 29: Tuplas


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta