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:
- Si se omite el primer índice (antes de los dos puntos) el trozo extraído comenzará al principio del string (o lista).
- Si se omite el segundo índice el trozo extraído se extenderá hasta el final del string (o lista)
- Si se provee un valor para m que es mayor que el tamaño del string (o lista), el trozo extraído tomará todos los valores hasta el final
- (no devolverá un error "fuera de rango" como hace la operación normal de indexación)
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:]?
- Respuestas:
- s[:] devuelve todo el string.
- friends[4] daría toda la lista a partir de la posición 4, o sea a partir de Zuki, que es el 5° elemento.
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".
- Lo veremos con detalle en el capítulo 11 sobre listas (sección 11.17).
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.
- La causa del problema: los caracteres « y » no están incluidos en la definición previa de puntuación ni en la de string.punctuation, por lo cual no se eliminan (no se cuentan como puntuación)
- Para solucionarlo, podemos agregar a la función eliminar_puntuacion una variable local (puntuacion_especial) que defina otros caracteres de puntuación, así: (L8_strings.py - eliminar_puntuacion_pro)
def eliminar_puntuacion_pro(s): puntuacion_especial = "«»" s_sin_puntuacion = "" for letra in s: if letra not in string.punctuation and letra not in puntuacion_especial: s_sin_puntuacion += letra return s_sin_puntuacion
- Esto se podría mejorar incluso más: permitir que el usuario pase como segundo argumento opcional la lista que define a puntuacion_especial (signos de puntuación suplementarios)
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:
- Si el campo tendrá alineación a la izquierda < derecha > o centrada ^
- El ancho que se le dará al campo en el string de resultado (un número como 10)
- El tipo de conversión (podemos forzar conversión a float, f, como en el ejemplo anterior, o bien pedir que los números enteros se conviertan a hexadecimales usando x)
- Si el tipo de conversión es un float, se puede también especificar cuántas cifras decimales se quiere (típicamente .2f se usa para manejar montos de dinero con 2 cifras decimales)
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"))
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").
- Caso con tab 8:
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
- Caso con tab 4:
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.
- Por ejemplo: for ch in "Ejemplo": ... ejecuta el cuerpo 7 veces con diferentes valores de ch cada 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.
- Ejemplos: "banana" < "manzana" evalúa a True, "pera" < "banana" evalúa a False y "Sandía" < "banana" evalúa a True porque todas las letras mayúsculas preceden a las minúsculas.
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.
- Ejemplos:
"ato" in "papas y boniatos"
evalúa a True."ñato" in "papas y boniatos"
evalúa a False.
8.18) Glosario
- tipo de datos compuesto, valor por defecto, docstring, notación punto (dot notation) y operador punto (dot operator), valor de datos inmutable (no se puede recortar ni modificar)
- índice (index), valor de datos mutable (se trata siempre de valores de tipos compuestos; las listas y diccionarios son mutables, pero los strings y tuplas no lo son)
- parámetro opcional, evaluación corto-circuito, recorrida,
- espacio en blanco (cualquiera de los caracteres que mueven el cursor sin imprimir - la variable string.whitespaces contiene todos los caracteres que son espacios en blanco)
- slice (trozo) (parte de un string (substring) especificado por un rango de índices
- más en general, para datos de cualquier tipo "secuencial" en Python se puede obtener una subsecuencia usando el operador slice
secuencia[start:stop]
)
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")
- La especificación es ambigua: no indica qué hacer con ocurrencias que se superponen entre sí, como en "ana" de "banana", que aparece dos veces pero superpuestas.
- Por lo tanto hay dos formas válidas de implementarlo. Se puede elegir cualquiera de las dos, y corresponden a los siguientes tests respectivamente:
test(remove_all("ana", "banana") == "bana") test(remove_all("ana", "banana") == "b")