Ciclos y secuencias: cadenas, tuplas, listas

¿Qué nombre debería asignar a mis variables de iteración?

Por convención, se utilizan nombres i, j, k para variables de iteración sobre números o posiciones.

for i in range(10):
    print(i)

Para todo lo demás, por ejemplo una iteración por elementos, debería utilizarse un nombre apto correspondiente a los valores que se están iterando:

personas = ['Alan', 'Barbara', 'Grace']
for persona in personas:
    print(persona)

¿Qué propiedad tienen los tipos de datos inmutables (como las cadenas y tuplas)?

Con los tipos de datos inmutables, es posible asignar un nuevo valor a las variables pero no es posible modificar su contenido. Por ejemplo con las cadenas:

>>> s = "ejemplo"
>>> s = "otro"
>>> s[2] = "c"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

Esto se debe a que cuando se realiza una nueva asignación, no se modifica la cadena en sí, sino que la variable s pasa a referenciar a otra cadena. En cambio, no es posible asignar un nuevo caracter en una posición, ya que esto implicaría modificar la cadena inmutable. Lo mismo sucede con las tuplas.

¿Qué propiedad tienen los tipos de datos mutables (como las listas)?

En el caso de los tipos de datos mutables, la asignación tiene el mismo comportamiento, es decir que las variables pasan a apuntar a un nuevo valor.

>>> lista1 = [10, 20, 30]
>>> lista2 = lista1
>>> lista1 = [3, 5, 7]
>>> lista1
[3, 5, 7]
>>> lista2
[10, 20, 30]

Algo importante a tener en cuenta en el caso de las variables de tipo mutable es que si hay dos o más variables que referencian a un mismo valor, y este valor se modifica, el cambio se verá reflejado en ambas variables.

>>> lista1 = [1, 2, 3]
>>> lista2 = lista1
>>> lista2[1] = 5
>>> lista1
[1, 5, 3]

Es importante remarcar que los parámetros mutables de funciones también hacen referencia a los valores originales. Si queremos que esto no suceda, necesitaríamos clonar la lista en algún punto del código.

>>> def cambia_lista(lista):
...     for i in range(len(lista)):
...         lista[i] = lista[i] ** 3
...
>>> lista = [1, 2, 3, 4]
>>> cambia_lista(lista)
>>> lista
[1, 8, 27, 64]

¿Hay alguna diferencia entre split() y split(' ')?

La documentación de help(str.split) indica que si no se provee la cadena de separador, se toma cualquier tipo de espacios consecutivos (sean el literal ' ' o incluso saltos de línea) como separadores, y también elimina cadenas vacías del resultado.

>>> s = ' hola  como  va   '
>>> s.split()
['hola', 'como', 'va']
>>> s.split(' ')
['', 'hola', '', 'como', '', 'va', '', '', '']

¿Qué problemas pueden ocurrir hay cuando se intenta modificar una lista que se está iterando?

Sin importar cómo se esté iterando una lista, es muy importante considerar qué sucedería si se agregan o eliminan elementos a la misma dentro del ciclo.

Considerando el siguiente ejemplo que elimina los elementos pares de una lista.

arr = [2, 4, 6, 3, 8, 7, 10]
for elem in arr:
    if elem % 2 == 0:
        arr.remove(elem)
print(arr)

Al ejecutarse el código, el programa muestra [4, 3, 7]. Lo que sucede es que al iterarse de izquierda a derecha, al borrar el 2 del comienzo de la lista hace que el ciclo se saltee el 4.

Si se intenta reemplazar el ciclo para que se itere por índice en vez de por elemento, además de saltearse elementos el programa lanzaría IndexError porque el range(len(arr)) ya habrá sido generado con la lista llena de elementos:

arr = [2, 4, 6, 3, 8, 7, 10]
for i in range(len(arr)):
    if arr[i] % 2 == 0:
        arr.pop(i)
print(arr)

Para este caso en particular se puede utilizar un ciclo indefinido aumentando un índice i de forma condicional, o bien se podría iterar por índice pero de forma inversa (desde el último hacia el primero). Por supuesto la posible solución va a depender del problema que se esté encarando, pero de cualquier forma siempre es importante tener en cuenta que modificar la cantidad de elementos de una lista mientras se está iterando puede traer errores.

arr = [2, 4, 6, 3, 8, 7, 10]
i = 0
while i < len(arr):
    if arr[i] % 2 == 0:
        arr.pop(i)
    else:
        i += 1
print(arr)
arr = [2, 4, 6, 3, 8, 7, 10]
for i in range(len(arr) - 1, -1, -1):
    if arr[i] % 2 == 0:
        arr.pop(i)
print(arr)

¿Qué significa empaquetar y desempaquetar valores?

Las secuencias se pueden desempaquetar en N variables diferentes si la cantidad de valores coincide perfectamente. Generalmente esto es común cuando se están manejando tuplas.

Por otro lado, se pueden empaquetar N variables en una única tupla.

>>> x = 2
>>> y = 3
>>> z = 1
>>> tupla = x, y, z
>>> tupla
(2, 3, 1)
>>> a, b, c = tupla
>>> b
3

Esto también se puede aprovechar a la hora de iterar una secuencia de tuplas, por ejemplo:

>>> invitados = [('Alan', 'Turing'), ('Barbara', 'Liskov'), ('Grace', 'Hopper')]
>>> for nombre, apellido in invitados:
...     print('Hola', nombre, apellido, '!')
...
Hola Alan Turing !
Hola Barbara Liskov !
Hola Grace Hopper !
>>>

Este concepto también se observó cuando se desempaquetaban los valores de una función que devolvía más de un elemento.

¿Existe un tipo de estructura en Python para representar una matriz?

No existe, pero con las estructuras de datos vistas hasta este punto del curso podemos modelar una matriz y realizar cualquier tipo de operación sobre esta.

En la sección del apunte 7.5 Listas y tuplas anidadas se hablas sobre matrices.

No puedo clonar una lista!

Las listas tienen el método copy(), pero es importante tener en cuenta cuáles son los tipos de datos que se están copiando.

Si la lista tiene elementos de un tipo de dato inmutable, entonces no habría problema con utilizar copy(). Como en los datos inmutables no se comparten referencias ni tampoco se pueden modificar el contenido de los mismos, la copia no traerá errores.

>>> l1 = ["hola", "como estas"]
>>> l2 = l1.copy()
>>> l1[0] = "che"
>>> l1
["che", "como estas"]
>>> l2
["hola", "como estas"]

En cambio si la lista tiene elementos de un tipo de dato mutable, no alcanza con hacer copy() porque las dos listas compartirían referencias a los mismos elementos! Por ejemplo con una lista de listas:

>>> m1 = [[1, 2], [3, 4]]
>>> m2 = m1.copy()
>>> m1[0][0] = 100
>>> m1
[[100, 2], [3, 4]]
>>> m2
[[100, 2], [3, 4]]

Una solución al problema de arriba es armar una nueva lista copiando todos los elementos de la lista original:

>>> m1 = [[1, 2], [3, 4]]
>>> m2 = [m1[0].copy(), m1[1].copy()]
>>> m1[0][0] = 100
>>> m1
[[100, 2], [3, 4]]
>>> m2
[[1, 2], [3, 4]]