Cap. 8 - Strings

8.1) Un tipo de datos compuesto

Hasta ahora hemos visto tipos de datos built-in como int, float, bool, str y hemos visto listas y pares. Los strings, las listas y los pares son cualitativamente distintos de los otros porque están hechos de piezas más pequeñas. En el caso de los strings, están hechos de strings más pequeños, cada uno de los cuales contiene un carácter. (siguiendo la tradicional terminología de la ciencia de la programación y la teoría de conjuntos, sería más correcto decir que los strings están hechos de carácteres - pero la terminología Python sólo tiene en cuenta la existencia del tipo string, no hay un tipo distinto para caracteres)

Los tipos que están hechos de piezas más pequeñas se llaman tipos compuestos. Según lo que estemos haciendo, querremos tratar a un dato compuesto como una cosa simple, o podemos querer acceder a sus partes. Dicha ambigüedad es útil.

8.2) Trabajando con strings como un todo

Vimos previamente que cada instancia de una tortuga tiene sus propios atributos y una cantidad de métodos que podemos aplicar a dicha instancia. Por ejemplo, podemos setear el color de la tortuga, y escribimos acciones como tess.turn(90).

Igual que la tortuga, un string también es un objeto. Así que cada instancia de un string tiene sus métodos y atributos.

Un ejemplo:

		>>> ss = "Hola, Mundo!"
		>>> tt = ss.upper()
		>>> tt
		'HOLA, MUNDO!'

El método upper puede ser llamado para cualquier string con el fin de crear un nuevo string, en el cual todos los caracteres son mayúsculas (el string original ss no se modifica).

Hay métodos como lower, capitalize y swapcase que cumplen con funciones similares, también útiles.

Para saber qué métodos están disponibles, puedes consultar la sección de Ayuda, buscar métodos de strings (string methods) y leerlo. O, si eres un poco vago, alcanza con que escribas esto en la consola de PyScripter:

		ss = "Hello, World!"
		tt = ss.

Después de escribir el punto que permite seleccionar uno de los métodos de ss, PyScripter mostrará una ventana de selección con todos los métodos (hay unos 70 - por suerte utilizaremos sólo unos pocos en este curso!) que pueden ser utilizados con un string.

Cuando escribes el nombre del método, un poco más de información (sobre sus parámetros y tipo de retorno) y su docstring serán desplegados. Este es un buen ejemplo de una herramienta (PyScripter) utilizando la meta-información (los docstrings) que proveen los programadores del módulo.

8.3) Trabajando con las partes de un string

El operador de indexación (Python usa paréntesis rectos para encerrar el índice) selecciona un substring de un sólo carácter de un string dado:

		>>> fruta = "banana"
		>>> m = fruta[1]
		>>> print(m)

La expresión fruta[1] selecciona el carácter número 1 de la palabra fruto, y crea un nuevo string que contiene sólo ese carácter. La variable m se refiere al resultado. Si pedimos que se despliegue m, nos llevamos una sorpresa: nos muestra una letra a.

		>>> print(m)
		a

Esto se debe a que los científicos de la computación siempre cuentan desde cero! La letra en la posición 0 de la palabra "banana" es una "b". Así que en posición 1 tenemos una "a".

Si queremos acceder a la primera letra de la palabra (o sea a la de índice 0), simplemente ponemos 0 o una expresión que evalúe a cero entre los paréntesis rectos:

		>>> m = fruta[0]
		>>> print(m)
		b

La expresión entre corchetes se llama índice. Un índice especifica a un miembro de una colección ordenada, en este caso la colección de caracteres de un string. El índice indica cuál de ellos queremos, y se puede utilizar como un índice el nombre del objeto (si lo tiene) o una expresión numérica.

Podemos usar enumerate para visualizar los índices:

		>>> fruta = "banana"
		>>> list(enumerate(fruta))
		[(0, 'b'), (1, 'a'), (2, 'n'), (3, 'a'), (4, 'n'), (5, 'a')]

Veremos más sobre enumerate en el capítulo en que estudiaremos las listas. Observar que Python no tiene un tipo especial para caracteres, son sólo strings de tamaño 1.

La misma notación de indexación se utiliza para extraer datos de cualquier tipo de lista:

		>>> numeros_primos = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]
		>>> numeros_primos[4]
		11
		>>> friends = ["Joe", "Zoe", "Brad", "Angelina", "Zuki", "Thandi", "Paris"]
		>>> friends[3]
		'Angelina'   

8.4) Length (largo de un string)

La función len, aplicada a un string, devuelve el número de caracteres en el mismo:

		>>> fruta = "banana"
		>>> len(fruta)
		6

Un error común es intentar obtener la última letra de una palabra con un código como este:

		sz = len(fruta)
		ultima_letra = fruit[sz]       # ERROR!

No funciona, y causa el error runtime IndexError: string index out of range. La razón es que no hay un carácter en la posición 6 en "banana", porque empezamos a contar las posiciones de las letras desde cero, y por lo tanto los 6 índices están numerados 0 a 5. Para obtener el último carácter, debemos restarle 1 al tamaño:

		sz = len(fruta)
		ultima_letra = fruit[sz - 1]

Alternativamente, podemos usar índices negativos, los cuales cuentan hacia atrás desde el final del string. La expresión fruta[-1] se refiere entonces a la última letra, fruta[-2] a la penúltima, y así sucesivamente.

Como habrás adivinado (correctamente), los índices negativos funcionan del mismo modo para listas.

No utilizaremos índices negativos en el resto de este curso - porque son pocos los lenguajes que usan esa forma de expresarse, y puede resultar un mal hábito en caso de que te acostumbres a usarlos. Pero está lleno de código Python en la web que usa el truco, así que es mejor que sepas de su existencia.

8.5) Recorrida (traversal) y el loop FOR

Muchas computaciones requieren recorrer un string carácter por carácter. Con frecuencia comienzan por el primer carácter, hacen algo con él, avanzan al siguiente, hacen algo con él, y siguen así hasta el final. Este patrón de procesamiento se llama recorrida (traversal) y una forma de codificarla es con una sentencia while: (L8_strings.py - recorrer_string_v1)

		def recorrer_string(fruta):
		    ix = 0;
		    while ix < len(fruta):
		        letra = fruta[ix]
		        print(letra)
		        ix += 1

Este loop recorre el string y muestra cada letra línea por línea. La condición del loop es ix < len(fruta), así que cuando ix es igual al tamaño del string la condición es falsa y no se ejecuta el body del loop. El último carácter que se accede es el de índice len(fruta) - 1 que es el último carácter en el string.

Pero hemos visto previamente cómo el loop for puede iterar fácilmente a través de los elementos de una lista y puede hacer lo mismo con un string: (L8_strings.py - recorrer_string)

		def recorrer_string(fruta):
		    for c in fruta:
		        print(c)

En cada iteración, el siguiente carácter del string es asignado a la variable c. El loop continúa hasta que no quedan caracteres. Aquí vemos el poder expresivo que nos da el loop for en comparación con el while a la hora de atravesar un string.

El siguiente ejemplo muestra cómo usar concatenación y un loop for para generar una serie ordenada alfabéticamente. Por ejemplo, en el libro Make Way for Ducklings de Robert McCloskey, los nombres de los ducklings son Jack, Kack, Lack, Mack, Nack, Ouack, Pack y Quack. El siguiente loop muestra dichos nombres en orden: (L8_strings.py - lista_ducklings)

		def lista_ducklings():
		    prefijos = "JKLMNOPQ"
		    sufijo = "ack"

		    for p in prefijos:
		        print(p + sufijo)

El output del programa es:

		Jack
		Kack
		Lack
		Mack
		Nack
		Oack
		Pack
		Qack

Por supuesto, esto no es 100% correcto porque los nombres Ouack y Quack quedaron mal escritos. Corregiremos este detalle en un ejercicio posterior.

8.6) Slices (trozos)

Un substring es un trozo (slice) del string. Del mismo modo podemos tomar una parte de una lista y hablar de que hemos tomado una sublista de la lista.

   		>>> s = "Piratas del Caribe"
		>>> print(s[0:7])
		Piratas
		>>> print(s[8:11])
		del
		>>> print(s[12:18])
		Caribe
		>>> friends = ["Joe", "Zoe", "Brad", "Angelina", "Zuki", "Thandi", "Paris"]
		>>> print(friends[2:4])
		['Brad', 'Angelina']

El operador [n:m] devuelve la parte del string que va del n° al m° carácter, incluyendo al primero pero sin incluir al último. Tal comportamiento tiene sentido si imaginas a los índices como señalando las posiciones entre caracteres, como en el siguiente diagrama:

Si imaginas esto como una pieza de papel, lo que hace el operador slice [n:m] es copiar la parte del papel que está entre las posiciones n y m. Suponiendo que n y m estén ambos dentro de los límites del string, el resultado tendrá tamaño (m - n).

Hay 3 trucos que se pueden agregar a lo anterior:

Por lo tanto:

		>>> fruta = "banana"
		>>> fruta[:3]
		'ban'
		>>> fruta[3:]
		'ana'
		>>> fruta[3:999]
		'ana'

¿Qué piensas que significa s[:]? ¿Y qué daría friends[4:]?

8.7) Comparación de strings

Los operadores de comparación funcionan con strings. Para ver si dos strings son iguales:

		if word == "banana":
		    print("Cierto, no tenemos bananas!") 

Otros operadores de comparación son útiles para ordenar las palabras lexicográficamente: (L8_strings.py - comparar_con_banana)

		    if palabra < "banana":
		        print("Tu palabra, " + palabra + ", va antes que banana.")
		    elif palabra > "banana":
		        print("Tu palabra, " + palabra + ", va después que banana.")
		    else:
		        print("Cierto, no tenemos bananas!")

Esto es similar al orden alfabético que usarías en un diccionario, pero con la diferencia de que todas las mayúsculas preceden a las minúsculas. Por lo tanto:

		Tu palabra, Zapallo, va antes que banana.

Una forma habitual de resolver este problema es convertir los strings a un formato estándar (por ejemplo, minúsculas) antes de hacer la comparación. Una dificultad mayor sería conseguir que el programa reconociera que "Zapallo" no es una fruta.

8.8) Los strings son inmutables

Puede ser tentador usar el operador [] del lado izquierdo de una asignación, para cambiar el carácter de un string. Por ejemplo:

		saludo = "Hola, mundo!"
		saludo[0] = 'J'            # ERROR!
		print(saludo)

En vez de producir el output Jola, mundo! este código producirá el error de tiempo de ejecución TypeError: 'str' object does not support item assignment.

Los strings son inmutables, lo que quiere decir que no se pueden modificar una vez que fueron creados. Lo más que se puede hacer es crear un nuevo string que sea una variación del original.

		saludo = "Hola, mundo!"
		nuevo_saludo = 'J' + saludo[1:]
		print(nuevo_saludo)

La solución aquí es concatenar una nueva primera letra con el trozo restante de saludo. Dicha operación no tiene efecto sobre el string original.

8.9) Los operadores IN y NOT IN

El operador in chequea membresía. Cuando ambos argumentos pasados a in son strings, in chequea si el argumento de la izquierda es un substring del argumento de la derecha.

		>>> "z" in "manzana"
		True
		>>> "i" in "manzana"
		False
		>>> "ma" in "manzana"
		True
		>>> "am" in "manzana"
		False

Observar que un string es un substring de sí mismo, y que el string vacío es un substring de cualquier otro (también observar que los programadores tienden a estudiar muy cuidadosamente esos casos de borde!)

El operador not retorna el resultado lógico opuesto, por lo cual:

		>>> "x" not in "manzana"
		True

Combinando el operador in con la concatenación de strings usando +, podemos escribir una función que elimine todas las vocales de un string: (L8_strings.py - quitar_vocales)

		def quitar_vocales(s):
		    vocales = "aeiouAEIOU"
		    s_sin_vocales = ""
		    for x in s:
		        if x not in vocales:
		            s_sin_vocales += x
		    return s_sin_vocales

		test(quitar_vocales("compsci") == "cmpsc")
		test(quitar_vocales("aAbEefIijOopUus") == "bfjps")

8.10) Una función FIND

Qué hace esta función? (respuesta: de ser posible devuelve la primer posición de un carácter ch en un string - o bien devuelve -1 si no lo encuentra en el string) (L8_strings.py - find_caracter)

def find_caracter(cadena, ch):
    """
      Encontrar y retornar el índice del carácter ch en el string cadena
      Devuelve -1 si ch no aparece en cadena.
    """
    ix = 0
    while ix < len(cadena):
        if cadena[ix] == ch:
            return ix
        ix += 1
    return -1

test(find_caracter("Compsci", "p") == 3)
test(find_caracter("Compsci", "C") == 0)
test(find_caracter("Compsci", "i") == 6)
test(find_caracter("Compsci", "x") == -1)

En cierto sentido, esta función es la inversa del operador de indexación. En vez de tomar un índice y devolver el carácter correspondiente, toma un carácter y devuelve el índice de su primera aparición en el string. Si el carácter no se encuentra, entonces devuelve -1.

Este es otro ejemplo en que vemos un enunciado return dentro de un loop. Si cadena[ix] == ch, la función retorna de inmediato, rompiendo el loop prematuramente.

Si el carácter no aparece en el string, entonces el programa sale del loop normalmente y retorna -1.

Este patrón de computación se llama a veces recorrida eureka o evaluación corto-circuito, porque tan pronto como encuentra lo que está buscando, corto-circuita y sale del loop.

8.11) Contando mediante un LOOP

El siguiente programa cuenta la cantidad de veces que la letra a aparece en un string, y es otro ejemplo del patrón de conteo que vimos en cantidad_digitos (sección 7.7) (L8_strings.py - cantidad_letra_a)

		def cantidad_letra_a(texto):
		    cantidad = 0
		    for c in texto:
		        if c == 'a':
		            cantidad += 1
		    return cantidad

		test(cantidad_letra_a("banana") == 3)

8.12) Parámetros opcionales

Para encontrar la segunda o tercera ocurrencia de una letra en un string, podemos modificar la función find_caracter (que a partir de ahora llamaremos find_caracter_v1 para evitar confusiones), agregando un tercer parámetro para la posición inicial en el string: (L8_strings.py - find_caracter_v2)

def find_caracter_v2(cadena, ch, inicio):
    """
      Encontrar y retornar el índice del carácter ch en el string cadena
      Busca sólo a partir del carácter en la posición dada por el parámetro inicio
      Devuelve -1 si ch no aparece en el trozo de cadena.
    """
    ix = inicio
    while ix < len(cadena):
        if cadena[ix] == ch:
            return ix
        ix += 1
    return -1

test(find_caracter("banana", "a", 2) == 3)

El llamado find_caracter("banana", "a", 2) devuelve 3, el índice de la primer ocurrencia de "a" en "banana" si comenzamos a buscar a partir del índice 2. Qué retornará find_caracter("banana", "n", 3)? Si respondiste 4, es muy probable que hayas comprendido cómo funciona find_caracter.

Todavía mejor, podemos unificar a nuestras dos versiones de find_caracter en una sola si usamos un parámetro opcional: (L8_strings.py - find_caracter_v3)

def find_caracter(cadena, ch, ini cio = 0):
    """
      Encontrar y retornar el índice del carácter ch en el string cadena
      Busca sólo a partir del carácter en la posición dada por el parámetro inicio
      El parámetro inicio vale 0 por defecto
      Devuelve -1 si ch no aparece en el trozo de cadena.
    """
    ix = inicio
    while ix < len(cadena):
        if cadena[ix] == ch:
            return ix
        ix += 1
    return -1

Cuando una función tiene un parámetro opcional, es posible llamarla pasando o no dicho parámetro. Si se provee tercer argumento a find_caracter, se asignará a inicio. Si no se le provee de tercer argumento, la función tomará el valor por defecto dado por la asignación inicio = 0 en su definición.

Por lo tanto, el llamado a find_caracter("banana", "a", 2) funcionará como lo hacía find_caracter_v2, mientras que el llamado a find_caracter("banana", "a") funcionará como lo hacía find_caracter_v1 (dado que inicio será seteado con el valor por defecto 0).

Si agregamos otro parámetro opcional a find_caracter podremos buscar entre una posición inicial y una posición final: (L8_strings.py - find_caracter)

def find_caracter(cadena, ch, inicio = 0, final = None):
    """
      Encontrar y retornar el índice del carácter ch en el string cadena
      Busca sólo entre los caracteres dados por las posiciones inicio y final
      El parámetro inicio vale 0 por defecto, y el final vale None, caso especial en que se lo ignora
      Devuelve -1 si ch no aparece en el trozo de cadena.
    """
    ix = inicio
    if final is None:
        final = len(cadena)
    elif final > len(cadena):
        final = len(cadena)
    while ix < final:
        if cadena[ix] == ch:
            return ix
        ix += 1
    return -1

Observación: las dos líneas violetas estaban ausentes en el curso original, pero sin ellas fallaría el llamado si se pasa por parámetro un valor de final mayor que el largo de la cadena (daría mensaje de error, producido en la línea que intenta a acceder a cadena[ix] para un ix fuera de rango).

El valor opcional final es interesante. Le dimos el valor por defecto None si el usuario lo pasa como argumento. En el body de la función entonces chequeamos si no se dio este caso: si vale None (es decir, el usuario no dio ningún argumento) reasignamos final al tamaño del string. Si en cambio el usuario sí nos pasó el valor como argumento, usamos ese valor (salvo en el caso en que exceda el largo del string, en el cual también utilizamos el tamaño del string: agregado al curso original porque era una errata)

La semántica de inicio y final en esta función son exactamente como el start y end de la función range.

Aquí hay algunos tests que la función debería pasar: (a futuro: crear una serie de tests equivalentes para una frase larga en español)

		ss = "Python strings have some interesting methods."
		test(find_caracter(ss, "s") == 7)
		test(find_caracter(ss, "s", 7) == 7)
		test(find_caracter(ss, "s", 8) == 13)
		test(find_caracter(ss, "s", 8, 13) == -1)
		test(find_caracter(ss, ".") == len(ss)-1)
		test(find_caracter(ss, "z", 3, 100) == -1)

Observación: el último caso fue agregado para testear el caso en que el usuario pasó un final mayor que el largo del texto, detalle que el curso original no manejaba bien.

8.13) El método built-in FIND

Ahora que hicimos todo este trabajo para escribir una poderosa función find_caracter, podemos revelar que ya existe un método built-in find, el cual puede hacer todo lo que hace nuestra función, y mucho más!

		test(ss.find("s") == 7)
		test(ss.find("s", 7) == 7)
		test(ss.find("s", 8) == 13)
		test(ss.find("s", 8, 13) == -1)
		test(ss.find(".") == len(ss)-1)
		test(ss.find("z", 3, 100) == -1)

Observación: que el test que agregamos funcione bien confirma que efectivamente había una errata en el código del curso original (de la sección 8.12), porque no hubiera pasado el test que sí pasa la función find de Python.

El método built-in find es más general que nuestro método. Puede encontrar substrings, y no sólo caracteres:

		>>> "banana".find("nan")
		2
		>>> "banana".find("na", 3)
		4

Es recomendable utilizar los métodos que provee Python en vez de ponerse a "reinventar la rueda". Pero en muchos casos implementar nuestras propias versiones de las funciones y métodos built-in de Python sirve como ejercicio y es útil para el aprendizaje: las técnicas que se van aprendiendo son pasos útiles para convertirse en un programador profesional.

8.14) El método SPLIT

Uno de los métodos más útiles para trabajar con strings es split, que divide un string formado por múltiples palabras en una lista de palabras individuales, eliminando todos los espacios en blanco entre ellas. (Por "espacios en blanco" nos referimos a cualquier carácter de espacio, tabulador o nueva línea que pudiera haber entre ellas). Esto nos permite convertir un string que contiene muchas palabras en una lista de esas palabras.

>>> ss = "Cada día sabemos más y entendemos menos (Albert Einstein)"
>>> palabras = ss.split()
>>> palabras
['Cada',  'día',  'sabemos',  'más',  'y',  'entendemos',  'menos',  '(Albert',  'Einstein)']

Observación: el método split permite customizar qué se considera como "espacio en blanco" o "separador de palabras".

8.15) Limpiando tus strings

Trabajaremos frecuentemente con strings que tienen signos de puntuación, espacios, tabuladores, nuevas líneas, especialmente (como haremos en futuros capítulos) cuando utilicemos texto extraído de archivos o de internet. Pero si estamos escribiendo un programa, por poner un ejemplo, que cuente la frecuencia de cada palabra o que revise la ortografía de las palabras, sería preferible poder eliminar esos caracteres indeseables.

Veremos sólo un ejemplo de cómo eliminar puntuación de un string. Recordemos que los strings son inmutables, por lo cual no podemos modificarlos: lo que debemos hacer es recorrer el string original y crear un nuevo string, que omita los signos de puntuación. (L8_strings.py - eliminar_puntuacion_v1)

puntuacion = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"

def eliminar_puntuacion_v1(s):
    s_sin_puntuacion = ""
    for letra in s:
        if letra not in puntuacion:
            s_sin_puntuacion += letra
    return s_sin_puntuacion

Setear la variable puntuacion, externa a la función, es peligroso y puede llevar a errores. Afortunadamente, el módulo string de Python lo hace por nosotros. Así que haremos una mejora a la versión anterior de la función, importando el módulo string y utilizando su definición de puntuación (string.punctuation), así: (L8_strings.py - eliminar_puntuacion)

		import string

		def eliminar_puntuacion(s):
		    s_sin_puntuacion = ""
		    for letra in s:
		        if letra not in string.punctuation:
		            s_sin_puntuacion += letra
		    return s_sin_puntuacion
   
		test(remove_punctuation('"Well, I never did!", said Alice.') == "Well I never did said Alice")
		test(remove_punctuation("Are you very, very, sure?") == "Are you very very sure")

Observación: Hay ciertos caracteres que nuestras funciones (tanto la versión inicial como la que se basa en string.punctuation) no eliminan, como se ve en este ejemplo:

print(eliminar_puntuacion("En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. Una olla de algo más vaca que carnero, salpicón las más noches, duelos y quebrantos los sábados, lantejas los viernes, algún palomino de añadidura los domingos, consumían las tres partes de su hacienda. El resto della concluían sayo de velarte, calzas de velludo para las fiestas, con sus pantuflos de lo mesmo, y los días de entresemana se honraba con su vellorí de lo más fino. Tenía en su casa una ama que pasaba de los cuarenta y una sobrina que no llegaba a los veinte, y un mozo de campo y plaza que así ensillaba el rocín como tomaba la podadera. Frisaba la edad de nuestro hidalgo con los cincuenta años. Era de complexión recia, seco de carnes, enjuto de rostro, gran madrugador y amigo de la caza. Quieren decir que tenía el sobrenombre de «Quijada», o «Quesada», que en esto hay alguna diferencia en los autores que deste caso escriben, aunque por conjeturas verisímiles se deja entender que se llamaba «Quijana». Pero esto importa poco a nuestro cuento: basta que en la narración dél no se salga un punto de la verdad."))

Se obtiene como output:

En un lugar de la Mancha de cuyo nombre no quiero acordarme no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero adarga antigua rocín flaco y galgo corredor Una olla de algo más vaca que carnero salpicón las más noches duelos y quebrantos los sábados lantejas los viernes algún palomino de añadidura los domingos consumían las tres partes de su hacienda El resto della concluían sayo de velarte calzas de velludo para las fiestas con sus pantuflos de lo mesmo y los días de entresemana se honraba con su vellorí de lo más fino Tenía en su casa una ama que pasaba de los cuarenta y una sobrina que no llegaba a los veinte y un mozo de campo y plaza que así ensillaba el rocín como tomaba la podadera Frisaba la edad de nuestro hidalgo con los cincuenta años Era de complexión recia seco de carnes enjuto de rostro gran madrugador y amigo de la caza Quieren decir que tenía el sobrenombre de «Quijada» o «Quesada» que en esto hay alguna diferencia en los autores que deste caso escriben aunque por conjeturas verisímiles se deja entender que se llamaba «Quijana» Pero esto importa poco a nuestro cuento basta que en la narración dél no se salga un punto de la verdad

Problema: no se removieron todos los caracteres especiales. Se ve por ejemplo «Quijana», sobre el final del texto.

Componiendo esta función con el método split visto en la sección anterior alcanzamos un doble objetivo: eliminar la puntuación y separar las palabras para obtener una lista de palabras:

frase = "En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. Una olla de algo más vaca que carnero, salpicón las más noches, duelos y quebrantos los sábados, lantejas los viernes, algún palomino de añadidura los domingos, consumían las tres partes de su hacienda. El resto della concluían sayo de velarte, calzas de velludo para las fiestas, con sus pantuflos de lo mesmo, y los días de entresemana se honraba con su vellorí de lo más fino. Tenía en su casa una ama que pasaba de los cuarenta y una sobrina que no llegaba a los veinte, y un mozo de campo y plaza que así ensillaba el rocín como tomaba la podadera. Frisaba la edad de nuestro hidalgo con los cincuenta años. Era de complexión recia, seco de carnes, enjuto de rostro, gran madrugador y amigo de la caza. Quieren decir que tenía el sobrenombre de «Quijada», o «Quesada», que en esto hay alguna diferencia en los autores que deste caso escriben, aunque por conjeturas verisímiles se deja entender que se llamaba «Quijana». Pero esto importa poco a nuestro cuento: basta que en la narración dél no se salga un punto de la verdad."
palabras = eliminar_puntuacion_pro(frase).split()
print(palabras)

El output:

['En', 'un', 'lugar', 'de', 'la', 'Mancha', 'de', 'cuyo', 'nombre', 'no', 'quiero', 'acordarme', 'no', 'ha', 'mucho', 'tiempo', 'que', 'vivía', 'un', 'hidalgo', 'de', 'los', 'de', 'lanza', 'en', 'astillero', 'adarga', 'antigua', 'rocín', 'flaco', 'y', 'galgo', 'corredor', 'Una', 'olla', 'de', 'algo', 'más', 'vaca', 'que', 'carnero', 'salpicón', 'las', 'más', 'noches', 'duelos', 'y', 'quebrantos', 'los', 'sábados', 'lantejas', 'los', 'viernes', 'algún', 'palomino', 'de', 'añadidura', 'los', 'domingos', 'consumían', 'las', 'tres', 'partes', 'de', 'su', 'hacienda', 'El', 'resto', 'della', 'concluían', 'sayo', 'de', 'velarte', 'calzas', 'de', 'velludo', 'para', 'las', 'fiestas', 'con', 'sus', 'pantuflos', 'de', 'lo', 'mesmo', 'y', 'los', 'días', 'de', 'entresemana', 'se', 'honraba', 'con', 'su', 'vellorí', 'de', 'lo', 'más', 'fino', 'Tenía', 'en', 'su', 'casa', 'una', 'ama', 'que', 'pasaba', 'de', 'los', 'cuarenta', 'y', 'una', 'sobrina', 'que', 'no', 'llegaba', 'a', 'los', 'veinte', 'y', 'un', 'mozo', 'de', 'campo', 'y', 'plaza', 'que', 'así', 'ensillaba', 'el', 'rocín', 'como', 'tomaba', 'la', 'podadera', 'Frisaba', 'la', 'edad', 'de', 'nuestro', 'hidalgo', 'con', 'los', 'cincuenta', 'años', 'Era', 'de', 'complexión', 'recia', 'seco', 'de', 'carnes', 'enjuto', 'de', 'rostro', 'gran', 'madrugador', 'y', 'amigo', 'de', 'la', 'caza', 'Quieren', 'decir', 'que', 'tenía', 'el', 'sobrenombre', 'de', 'Quijada', 'o', 'Quesada', 'que', 'en', 'esto', 'hay', 'alguna', 'diferencia', 'en', 'los', 'autores', 'que', 'deste', 'caso', 'escriben', 'aunque', 'por', 'conjeturas', 'verisímiles', 'se', 'deja', 'entender', 'que', 'se', 'llamaba', 'Quijana', 'Pero', 'esto', 'importa', 'poco', 'a', 'nuestro', 'cuento', 'basta', 'que', 'en', 'la', 'narración', 'dél', 'no', 'se', 'salga', 'un', 'punto', 'de', 'la', 'verdad']

Hay otros métodos útiles de string, pero este curso no busca ser un manual de referencia. Para eso ya existe la Python Reference Library, y mucha más documentación, la cual puede encontrarse en el sitio web de Python

8.16) El método FORMAT de string

La forma más sencilla y rápida de formatear un string en Python es mediante el método format. Para ver cómo funciona, comencemos con algunos ejemplos:

s1 = "Su nombre es {0}!".format("Arturo")
print(s1)

nombre = "Alicia"
edad = 10
s2 = "Me llamo {1} y tengo {0} años.".format(edad, nombre)
print(s2)

n1 = 4
n2 = 5
s3 = "2**10 = {0} y {1} * {2} = {3:f}".format(2**10, n1, n2, n1 * n2)
print(s3)

Al ejecutar el script se obtiene:

		Su nombre es Arturo!
		Me llamo Alicia y tengo 10 años.
		2**10 = 1024 y 4 * 5 = 20.000000

El string plantilla (template) contiene marcadores de posición (placeholders) ... {0} ... {1} ... {2} ... etc. El método format sustituye los marcadores de posición con sus argumentos. Los números en los marcadores (placeholders) son índices que determinan qué argumento es sustituido - ver con detenimiento la línea del ejemplo en que los argumentos son edad y nombre.

Pero aun hay más: cada uno de los marcadores de posición puede tener una especificación de formato (que siempre comienza con el símbolo : como se ve por ejemplo en la línea que define a s3 en el ejemplo anterior. Esto modifica cómo se hacen las sustituciones en la plantilla y permite controlar cosas como:

Veamos unos cuantos ejemplos sencillos que cubrirán casi todos los casos típicamente necesarios. Si se necesita hacer algo más rebuscado, es recomendable leer el help y enterarse de las abundantes opciones disponibles.

n1 = "José"
n2 = "Gervasio"
n3 = "Artigas"

print("Pi con 3 cifras decimales es {0:.3f}".format(3.1415926))
print("123456789 123456789 123456789 123456789 123456789 123456789")
print("|||{0:<15}|||{1:^15}|||{2:>15}|||Nacido en {3}|||".format(n1,n2,n3,1764))
print("El valor decimal {0} se expresa en notación hexadecimal como {0:x}".format(123456))

Este script produce el output siguiente:

Pi con 3 cifras decimales es 3.142
123456789 123456789 123456789 123456789 123456789 123456789
|||José           |||   Gervasio    |||        Artigas|||Nacido en 1764|||
El valor decimal 123456 se expresa en notación hexadecimal como 1e240

Se puede tener múltiples marcadores indexando el mismo argumento, o tener argumentos extra que no son referenciados en los marcadores: (L8_strings.py - carta_formateable)

def carta_formateable():
    carta = """
Querido {0} {2}.
 {0}, tengo una interesante propuesta de negocios para hacerte!
 Si depositas $10 millones en mi cuenta bancaria, puedo duplicar tu dinero ...
        """

    print(carta.format("Jeff", "Preston", "Bezos"))
    print(carta.format("Bill", "Henry", "Gates"))

Lo que produce el siguiente output:

		Querido Jeff Bezos.
		 Jeff, tengo una interesante propuesta de negocios para hacerte!
		 Si depositas $10 millones en mi cuenta bancaria, puedo duplicar tu dinero ...

		Querido Bill Gates.
		 Bill, tengo una interesante propuesta de negocios para hacerte!
		 Si depositas $10 millones en mi cuenta bancaria, puedo duplicar tu dinero ...

Como imaginarás, recibirás un mensaje de error en caso de que tus marcadores se refieran a un argumento que te faltó pasar:

		>>> "hola {3}".format("Felipe")
		Traceback (most recent call last):
		  File "", line 1, in 
		IndexError: tuple index out of range

El siguiente ejemplo ilustra cuál es la utilidad real de formatear strings. Primero, intentaremos imprimir una tabla sin usar formateo de strings: (L8_strings.py - tabla_potencias_hasta_10_sin_formatear)

def tabla_potencias_hasta_10_sin_formatear():
    print("i\ti**2\ti**3\ti**5\ti**10\ti**20")
    for i in range(1, 11):
        print(i, "\t", i**2, "\t", i**3, "\t", i**5, "\t",
                                            i**10, "\t", i**20)

Este programa imprime una tabla de varias potencias de los números 1 al 10. (En algunas configuraciones de Python el tab tendrá tamaño 8 y en otras tamaño 4. El resultado que sigue aquí es para un tab 4, pero en ambos casos el output se verá "desaliñado").

i       i**2    i**3    i**5    i**10   i**20
1       1       1       1       1       1
2       4       8       32      1024    1048576
3       9       27      243     59049   3486784401
4       16      64      1024    1048576         1099511627776
5       25      125     3125    9765625         95367431640625
6       36      216     7776    60466176        3656158440062976
7       49      343     16807   282475249       79792266297612001
8       64      512     32768   1073741824      1152921504606846976
9       81      729     59049   3486784401      12157665459056928801
10      100     1000    100000  10000000000     100000000000000000000

i	i**2	i**3	i**5	i**10	i**20
1 	 1 	 1 	 1 	 1 	 1
2 	 4 	 8 	 32 	 1024 	 1048576
3 	 9 	 27 	 243 	 59049 	 3486784401
4 	 16 	 64 	 1024 	 1048576 	 1099511627776
5 	 25 	 125 	 3125 	 9765625 	 95367431640625
6 	 36 	 216 	 7776 	 60466176 	 3656158440062976
7 	 49 	 343 	 16807 	 282475249 	 79792266297612001
8 	 64 	 512 	 32768 	 1073741824 	 1152921504606846976
9 	 81 	 729 	 59049 	 3486784401 	 12157665459056928801
10 	 100 	 1000 	 100000 	 10000000000 	 100000000000000000000

El código asume que el carácter tab ( \t) alineará las columnas, pero esta suposición falla cuando los valores a imprimir en la tabla superan el ancho del tabulador.

Una posible solución sería cambiar el ancho de tabulación, pero la primera columna ya tiene más espacio del que necesita (y si cambiamos el ancho de tabulación, estaremos extendiendo todas las columnas por igual). La mejor solución sería setear el ancho de cada columna independientemente. El formateo de string permite así una solución mucho más conveniente, y podemos incluso alinear todos los campos a la derecha. Lo conseguimos utilizando un código como este: (L8_strings.py - tabla_potencias_hasta_10_formateando)

def tabla_potencias_hasta_10_formateando():
    layout = "{0:>4}{1:>6}{2:>6}{3:>8}{4:>13}{5:>24}"
    print(layout.format("i", "i**2", "i**3", "i**5", "i**10", "i**20"))
    for i in range(1, 11):
        print(layout.format(i, i**2, i**3, i**5, i**10, i**20))

Al ejecutar esta versión obtenemos este output, mucho más agradable a la vista:

 i  i**2  i**3    i**5        i**10                   i**20
 1     1     1       1            1                       1
 2     4     8      32         1024                 1048576
 3     9    27     243        59049              3486784401
 4    16    64    1024      1048576           1099511627776
 5    25   125    3125      9765625          95367431640625
 6    36   216    7776     60466176        3656158440062976
 7    49   343   16807    282475249       79792266297612001
 8    64   512   32768   1073741824     1152921504606846976
 9    81   729   59049   3486784401    12157665459056928801
10   100  1000  100000  10000000000   100000000000000000000

8.17) Resumen

Este capítulo introdujo una cantidad de nuevas ideas. El siguiente resumen puede ayudar a recordar lo aprendido:

Indexado ([]) - Acceso a un carácter de un string a partir de su posición (que se cuenta desde 0). Ejemplo: "Esto"[2] evalúa a "t".

Función len (largo) - Devuelve la cantidad de caracteres de un string. Ejemplo: len("Feliz") evalúa a 5.

Recorrida for - Recorrer o atravesar un string accediendo cada uno de sus caracteres, uno por vez.

Slicing ([ : ]) - Un trozo (slice) es un substring de un string. Ejemplo: "bananas y chocolate"[3:6] evalúa a ana (y lo mismo hace "bananas y chocolate[1:4])

Comparación de strings (<, >, <=, >=, ==, !=) - los 6 operadores de comparación numérica también funcionan con strings, evaluando según el orden lexicográfico.

Operadores in y not in (in, not in) - El operador in verifica membresía. En el caso de de strings, verifica si un string está contenido o no en otro.

8.18) Glosario

8.19) Ejercicios

Para estos ejercicios, sugerimos crear un archivo con todos los llamados a test de los capítulos previos, y poner a todas las funciones que requieren testeo dentro de ese archivo.

 

1) Cuál es el resultado de cada uno de los siguientes? (hacerlo y traducirlo)

>>> "Python"[1]
>>> "Strings are sequences of characters."[5]
>>> len("wonderful")
>>> "Mystery"[:4]
>>> "p" in "Pineapple"
>>> "apple" in "Pineapple"
>>> "pear" not in "Pineapple"
>>> "apple" > "pineapple"
>>> "pineapple" < "Peach" 

 

2) Modificar el siguiente programa para que Ouack y Quack se escriban correctamente: (hacerlo - se trata de modificar el programa lista_ducklings implementado en este capítulo)

    prefijos = "JKLMNOPQ"
    sufijo = "ack"

    for p in prefijos:
        print(p + sufijo)

 

3) Encapsular el siguiente código en una función llamada contar_letras, y generalizarlo para que acepte el string y la letra como argumentos. Hacer que la función devuelva la cantidad de caracteres, en vez de imprimir la respuesta. El llamador de la función debería hacer la impresión. (hacerlo)

fruta = "banana"
cuenta = 0
for caracter in fruta:
    if caracter == "a":
        cuenta += 1
print(cuenta)

 

4) Ahora reescribe la función contar_letras para que en vez de recorrer el string, llame repetidamente al método find, con el tercer parámetro opcional para ubicar nuevas ocurrencias de la letra que se está contando. (hacerlo)

 

5) Asignar a una variable un string triple-encomillado que contenga un párrafo de texto que te guste (un poema, un discurso, una receta de cocina, etc.). Luego escribe una función que elimine toda la puntuación del texto, lo descomponga en una lista de palabras, y cuente la cantidad de palabras que contienen la letra "e". Tu programa debería imprimir un análisis del texto así: (hacerlo)

		Tu texto contiene 243 palabras, de las cuales 109 (44.8%) contienen la letra "e".

 

6) Imprimir una tabla de multiplicación bien formateada (visualmente), hasta 12 x 12. (hacerlo)

 

7) Escribe una función que invierta las letras de un string, y pase estos tests: (hacerlo - y traducir tests)

		test(reverse("happy") == "yppah")
		test(reverse("Python") == "nohtyP")
		test(reverse("") == "")
		test(reverse("a") == "a")

 

8) Escribe una función que espeje sus argumentos: (hacerlo - y traducir tests)

		test(mirror("good") == "gooddoog")
		test(mirror("Python") == "PythonnohtyP")
		test(mirror("") == "")
		test(mirror("a") == "aa")

 

9) Escribe una función que elimine todas las ocurrencias de una letra de un string: (hacerlo - y traducir tests)

		test(remove_letter("a", "apple") == "pple")
		test(remove_letter("a", "banana") == "bnn")
		test(remove_letter("z", "banana") == "banana")
		test(remove_letter("i", "Mississippi") == "Msssspp")
		test(remove_letter("b", "") = "")
		test(remove_letter("b", "c") = "c")

 

10) Escribe una función que reconozca palíndromos. (Pista: utiliza tu función reverse (ejercicio 7) para simplificar el trabajo) (hacerlo - y traducir tests)

		test(is_palindrome("abba"))
		test(not is_palindrome("abab"))
		test(is_palindrome("tenet"))
		test(not is_palindrome("banana"))
		test(is_palindrome("straw warts"))
		test(is_palindrome("a"))
		# test(is_palindrome(""))    # Is an empty string a palindrome?

 

11) Escribe una función que cuente cuántas veces un substring ocurre en un string: (hacerlo - y traducir tests)

		test(count("is", "Mississippi") == 2)
		test(count("an", "banana") == 2)
		test(count("ana", "banana") == 2)
		test(count("nana", "banana") == 1)
		test(count("nanan", "banana") == 0)
		test(count("aaa", "aaaaaa") == 4)

 

12) Escribe una función que elimine la primer ocurrencia de un string de otro string: (hacerlo - y traducir tests)

		test(remove("an", "banana") == "bana")
		test(remove("cyc", "bicycle") == "bile")
		test(remove("iss", "Mississippi") == "Missippi")
		test(remove("eggs", "bicycle") == "bicycle")

 

13) Escribe una función que elimine todas las ocurrencias de un string de otro string: (hacerlo - y traducir tests)

		test(remove_all("an", "banana") == "ba")
		test(remove_all("cyc", "bicycle") == "bile")
		test(remove_all("iss", "Mississippi") == "Mippi")
		test(remove_all("eggs", "bicycle") == "bicycle")

			test(remove_all("ana", "banana") == "bana")
			test(remove_all("ana", "banana") == "b")