¿Qué vamos a ver en este artículo? La idea básica es ver cómo trabajar con cadenas de texto en Python.
- Formas de formatear cadenas.
- Problemas a solucionar.
- La forma antigua.
- Usando el método
formatde los objetosstr(o la funciónformat). - Usando f-strings.
- Usando el objeto
string.Template. - Modificar, separar, reemplazar,...
Primero de todo voy a importar la siguiente biblioteca:
import string
¿Por qué no concatenar texto? Porque puede ser más lento, porque puede ser menos mantenible, porque puede ser más lioso, porque puede ser más propenso a errores, porque puede ser menos legible,...
La idea principal del artículo es convenceros de que algo como lo siguiente, concatenar texto, siempre será peor en la mayoría de las ocasiones:
nombre = "Kiko"
fecha = "10/10/2010"
lugar = "la Luna"
print(nombre + " lleva en " + lugar + " desde " + fecha)
Kiko lleva en la Luna desde 10/10/2010
Formas de formatear texto
Tenemos varias formas:
- Usando la forma antigua (old style o printf-style) mediante el uso del operador
%para texto. Es usable en CPython 2 y CPython 3. - Usando el método
formatde un objetostro la funciónformat. Es compatible con versiones de CPython superiores o igual a 2.6. - Usando f-strings. Introducido en la versión de Python 3.6. Pypy lo incluye desde su versión 6.0 (que es compatible con CPython 3.5).
- Usando el objeto
Templatedel módulostring. Está disponible desde hace más de 15 años tanto en Python 2 como en Python 3 (commit).
Problemas a solucionar
Vamos a formatear y modificar texto de tal forma que sea sencillo crear una plantilla para un correo o para meter partes de un informe de forma sencilla o para actualizar las partes de una nueva factura,... Es decir, esto tendrá sentido cuando tenemos que actualizar partes de un texto y el resto se mantiene sin cambios de forma que la automatización resulte de gran ayuda.
La forma antigua
No te voy a enseñar como usarla. No la uses. En la documentación de las nuevas versiones de Python aparece muy poco para desincentivar su uso,... En algún sitio de la documentación dicen:
The formatting operations described here exhibit a variety of quirks that lead to a number of common errors (such as failing to display tuples and dictionaries correctly). Using the newer formatted string literals, the str.format() interface, or template strings may help avoid these errors. Each of these alternatives provides their own trade-offs and benefits of simplicity, flexibility, and/or extensibility.
que, traducido, sería algo así como:
Las operaciones de formateado mostradas a continuación presentan una serie de peculiaridades que pueden llevar a cometer una serie de errores típicos (como que falle a la hora de mostrar correctamente tuplas y diccionarios). Usando los nuevos strings formateados de forma literal (f-strings), la interfaz
str.format()o las plantillas de strings pueden ayudar a evitar este tipo de errores. Cada una de estas alternativas proporciona sus propias contrapartidas y ventajas de simplicidad, flexibilidad y/o extensibilidad.
Pros:
- Compatible con versiones viejas de Python. Si te ha tocado lidiar con código legado quizás tengas que usarlo. Aunque el siguiente método que voy a comentar, el método
formatde los objetosstr, es compatible con Python 2.6 y 2.7 y podrías usar ese método. Si tu código a mantener es más antiguo que python 2.6 lo siento mucho por ti.
Contras:
- Es feo.
- Puede llevar a errores (ver ejemplo más abajo).
- Tienes que respetar el orden de inserción (*).
- No puedes formatear fechas, no puedes usar la clave o valor de diccionarios,...
- Puede confundirse con el operador módulo para números. Aunque esto puede suceder con el operador suma y multiplicación con los strings por lo que este argumento no tiene tanto peso.
- ...
# Esto funciona
a = (1, 2, 3)
print("%s" % (a,))
# Esto no funciona con old-style
# pero si funciona con .format o f-strings
a = (1, 2, 3)
print("%s" % a)
(1, 2, 3)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-3-6c8b05e4cdb8> in <module>
6 # pero si funciona con .format o f-strings
7 a = (1, 2, 3)
----> 8 print("%s" % a)
TypeError: not all arguments converted during string formatting
Usando el método format
Esta forma existe para mejorar muchos de los problemas que había con la forma antigua. Existe un mini lenguaje que ayuda a representar cierta información que se puede usar tanto con este método, con los f-strings como con la función format, la cual funciona de forma muy similar al método format y por eso la estoy obviando en este tutorial.
La especificación del mini lenguaje de formateo de texto la puedes encontrar aquí. A lo largo de lo que resta de tutorial vamos a hacer uso de este mini lenguaje e intentaré explicar qué es lo que estoy haciendo en cada momento.
Para usar el método format podemos hacer lo siguiente (recupero el primer ejemplo):
nombre = "Kiko"
fecha = "10/10/2010"
lugar = "la Luna"
texto = "{nombre} lleva en {lugar} desde {fecha}"
print(texto.format(nombre=nombre, lugar=lugar, fecha=fecha))
Kiko lleva en la Luna desde 10/10/2010
Con lo anterior ya podemos ver algunas mejoras con respecto a la forma antigua. texto es más legible. Podríamos reutilizar variables:
nombre = "Kiko"
fecha = "10/10/2010"
lugar = "la Luna"
texto = "{nombre} lleva en {lugar} desde {fecha}. Pobre {nombre}"
print(texto.format(nombre=nombre, lugar=lugar, fecha=fecha))
Kiko lleva en la Luna desde 10/10/2010. Pobre Kiko
Podríamos formatear fechas:
import datetime as dt
import locale
locale.setlocale(locale.LC_ALL, 'es_ES')
nombre = "Kiko"
fecha = dt.date(2010, 10, 10)
lugar = "la Luna"
# Ver https://docs.python.org/3.8/library/datetime.html#strftime-and-strptime-format-codes
texto = (
"{nombre} lleva en {lugar} "
"desde el {fecha: %A}, {fecha: %d} de {fecha: %B} del {fecha: %Y}. "
"Pobre {nombre}"
)
print(texto.format(nombre=nombre, lugar=lugar, fecha=fecha))
Kiko lleva en la Luna desde el domingo, 10 de octubre del 2010. Pobre Kiko
En este ejemplo podéis ver que estoy usando : después de la variable que va entre claves ({...:...}) para especificar una serie de cosas de cómo quiero representar las variables. Ahí es donde entra en acción el mini lenguaje de formateado. En el ejemplo anterior hemos formateado una fecha (ver el comentario con una URL en el código para saber más).
Veamos algunos ejemplo de cómo usarlo:
texto = "Tengo {anyos: d} años."
print(texto.format(anyos=15))
Tengo 15 años.
Lo anterior escribe enteros en notación decimal. Si lo quiero escribir en hexadecimal, por la razón que sea, podría usar:
texto = "Tengo {anyos: x} años."
print(texto.format(anyos=15))
Tengo f años.
Lo puedo escribir también con decimales:
texto = "Tengo {anyos: .2f} años."
print(texto.format(anyos=15))
Tengo 15.00 años.
texto = "Tengo {anyos: .1f} años."
print(texto.format(anyos=15))
Tengo 15.0 años.
En los dos ejemplos anteriores hemos formateado números decimales indicándole que muestre uno o dos decimales mediante el uso de .2f o .1f.
Para ver más sobre esto le podéis echar un ojo a este otro artículo que escribí hace un tiempo donde se entra más en detalle en el funcionamiento del mini lenguaje de los strings.
Usando el método format podemos hacer más cosas que no podemos con la forma antigua. Algunos ejemplos más, este primero ya lo hemos visto más arriba:
texto = "El resultado, mostrado como tupla, es: {res}"
print(texto.format(res=(1, 2, 3)))
El resultado, mostrado como tupla, es: (1, 2, 3)
También podemos pasar parámetros usando orden:
texto = "{0} + {0} = {1}"
print(texto.format(1, 2))
1 + 1 = 2
En el ejemplo anterior uso el orden de los *args pasados al método donde 1 está en la posición '0' y 2 está en la posición '1' y de esa forma los puedo reusar donde quiera, como he hecho con 1.
Se pueden usar atributos de objetos:
class Dummy:
def init(self, name, age):
self.name = name
self.age = age
dummy = Dummy("kiko", 12)
texto = "{dummy.name} tiene {dummy.age} años."
print(texto.format(dummy=dummy))
kiko tiene 12 años.
Podrías hacer algo similar con diccionarios.
Bueno, todo esto está muy bien y estamos viendo que tenemos mejoras con respecto a la forma antigua. Vamos a dar un paso más allá y a empezar a usar los f-strings que son incluso más potentes.
Usando f-strings
Voy a empezar extendiendo el ejemplo de antes para mostrar una de las potenciales ventajas de los f-strings frente a otros métodos.
class Dummy:
def init(self, name, age):
self.name = name
self.age = age
def meses(self):
return self.age * 12
dummy = Dummy("kiko", 12)
texto = "{dummy.name} tiene {dummy.age} años que son {dummy.meses()} meses."
print(texto.format(dummy=dummy))
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-16-6d5292581545> in <module>
11 texto = "{dummy.name} tiene {dummy.age} años que son {dummy.meses()} meses."
12
---> 13 print(texto.format(dummy=dummy))
AttributeError: 'Dummy' object has no attribute 'meses()'
¡¡Da un error!! ¿Por qué? Seguimos y lo explicamos a continuación...
Rehago lo anterior usando f-strings:
class Dummy:
def init(self, name, age):
self.name = name
self.age = age
def meses(self):
return self.age * 12
dummy = Dummy("kiko", 12)
texto = f"{dummy.name} tiene {dummy.age} años que son {dummy.meses()} meses."
print(texto)
kiko tiene 12 años que son 144 meses.
Vemos varias cosas: el código es más conciso lo que podría ayudar a evitar errores, por tener menos código y que, por tanto, sea más mantenible y, además, podemos ejecutar, por ejemplo, funciones o métodos como estoy haciendo aquí con el método dummy.meses, algo que el método format no me ha permitido como has visto en el ejemplo anterior.
Podríamos evaluar otras cosas. Por ejemplo, con los f-strings podemos evaluar expresiones:
cien = 100
dos = 2
texto = f"la suma de {cien:.1e} más {dos:E} es igual a {sum((cien, dos)):.5f}"
print(texto)
la suma de 1.0e+02 más 2.000000E+00 es igual a 102.00000
En los ejemplos anteriores uso notación exponencial para que veamos más ejemplos de formateos.
Usando Template
Esta es una forma menos potente pero que te puede resolver problemas concretos de forma sencilla. En este caso, lo que se usa es el símbolo del dólar, $, para poder usar variables dentro del texto.
t = string.Template("Hola, ¿cómo estás, $name?")
print(t.substitute(name="Leia"))
Hola, ¿cómo estás, Leia?
Si quisiéramos usar el símbolo de dólar como parte del textolo tendríamos que hacer así:
def su_cambio(cantidad):
t = string.Template("Su cambio son $quantity escribirá el símbolo de dolar.
Y no podríamos hacer mucho más... Entonces, ¿para qué usar usar string.Template si es tan limitado? Debido a esas limitaciones puede ser una buena opción si en el texto que hemos de formatear queremos evitar que se ejecute código. Con otras opciones hemos visto que cierto código puede ser ejecutado. Si nosotros no somos los responsables de meter lo que se puede evaluar en el formateado podríamos tener algún problema de seguridad y, en esos casos, string.Template podría ser una posible opción a usar.
Métodos de un objeto str
Además de poder formatear texto y números de muchas formas, el propio objeto str dispone de muchos métodos que pueden resultar muy útiles para que el texto se muestre como deseamos. Veamos algunos...
Por ejemplo, imaginad que os llega una información que no puede tener espacios de ningún tipo (espacios, saltos de línea,...). Podéis usar el método replace para eliminar estos espacios:
texto = "Nombre en clave"
print(texto.replace(" ", ""))
Nombreenclave
Pero lo anterior tiene el problema de que tenemos que especificar otros tipos de 'espacios'. Por ejemplo, lo siguiente no me eliminaría los saltos de línea
texto = "Nombre en\nclave"
print(texto.replace(" ", ""))
Nombreen
clave
Con string.whitespace podemos ver lo que son espacios:
string.whitespace
' \t\n\r\x0b\x0c'
Mi problema anterior lo podría resolver chapuceramente de la siguiente forma:
texto = "Nombre en\nclave"
print(texto.replace(" ", "").replace("\n", ""))
Nombreenclave
Es decir, concatenando llamadas a replace con cada uno de los posibles espacios (espacios en blanco, saltos de linea, tabulaciones,...)... Pero lo anterior no es mantenible ni bonito. Una posible solución podría ser usando los métodos strip y join. El primero lo que hace es separar el texto usando el delimitador que le indique y el segundo lo que hace es unirme partes de texto con el delimitador que considere. Veamos primero uno y luego otro y luego uniremos sus fortalezas para resolver el problema anterior:
texto = "Nombre \ten\nclave"
print(texto.split())
['Nombre', 'en', 'clave']
Al método split, si no indico el separador a usar por defecto separa el texto por sus espacios (espacio en blanco, tabulación, salto de línea,...).
En método join lo que hace es unir partes:
','.join(('a', 'b', 'c'))
'a,b,c'
Entre cada dos valores intercala la coma que he usado en el string. Unamos el uso de ambos métodos para resolver el problema original, el texto sin ningún tipo de espacios:
texto = "Nombre \nen\nclave"
print("".join(texto.split()))
Nombreenclave
También nos podría interesar que, antes de unir el texto, cada palabra empezase por mayúscula para así, después de quitar los espacios, poder leer el resultado de forma más sencilla. Para ello podríamos usar el método title:
texto = "Nombre \nen\nclave"
# cada palabra en mayúscula
texto = texto.title()
print(texto)
print("".join(texto.split()))
Nombre
En
Clave
NombreEnClave
Si solo quisíeramos en mayúscula la primera palabra del texto podríamos usar el método capitalize.
Quizá queremos un texto que al guardarlo quede de tal forma que todos los signos de puntuación (',', '.', '!', '¿',...) sean sustituidos por un guión bajo ('_'). Además, queremos que las mayúsculas se conviertan en minúsculas y que los espacios se conviertan en guiones ('-'). Por ejemplo, queremos que el texto:
Me gustas cuando callas porque estás como ausente,
y me oyes desde lejos, y mi voz no te toca.
Se convierta finalmente en:
me-gustas-cuando-callas-porque-estás-como-ausente_y-me-oyes-desde-lejos_-y-mi-voz-no-te-toca_
Es decir, todo el texto en minúsculas, los espacios y saltos de línea convertidos en guiones y los signos de puntuación convertidos en guiones bajos.
Para convertir todo a minúsculas podemos usar el método lower (para convertir todo a mayúscula podemos usar el método upper):
texto = """Me gustas cuando callas porque estás como ausente,
y me oyes desde lejos, y mi voz no te toca."""
print(texto.lower())
me gustas cuando callas porque estás como ausente,
y me oyes desde lejos, y mi voz no te toca.
Para transformar los espacios ya hemos visto el ejemplo anterior. Modifico el ejemplo anterior con el nuevo texto y el nuevo separador y encadeno, además, el método lower:
texto = """Me gustas cuando callas porque estás como ausente,
y me oyes desde lejos, y mi voz no te toca."""
print("-".join(texto.lower().split()))
me-gustas-cuando-callas-porque-estás-como-ausente,-y-me-oyes-desde-lejos,-y-mi-voz-no-te-toca.
Para cambiar los signos de puntuación voy a hacer uso de dos métodos, maketrans y translate. El primero me crea un mapeo de las cosas que quiero eliminar del texto a las cosas por las que las quiero sustituir. Para indicar los signos de puntuación voy a hacer uso de string.punctuation
print(string.punctuation)
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
Primero hago el mapeo que luego usaré en translate:
mapping = texto.maketrans(string.punctuation, "_" * len(string.punctuation))
print(mapping)
{33: 95, 34: 95, 35: 95, 36: 95, 37: 95, 38: 95, 39: 95, 40: 95, 41: 95, 42: 95, 43: 95, 44: 95, 45: 95, 46: 95, 47: 95, 58: 95, 59: 95, 60: 95, 61: 95, 62: 95, 63: 95, 64: 95, 91: 95, 92: 95, 93: 95, 94: 95, 95: 95, 96: 95, 123: 95, 124: 95, 125: 95, 126: 95}
Lo anterior dice que las ',' las identifique como '_', los '.' como '_', los '?' como '_',..., a la hora de hacer la transformación. La transformación la hago con translate usando ese mapping:
texto = """Me gustas cuando callas porque estás como ausente,
y me oyes desde lejos, y mi voz no te toca."""
mapping = texto.maketrans(string.punctuation, "_" * len(string.punctuation))
print(texto.translate(mapping))
Me gustas cuando callas porque estás como ausente_
y me oyes desde lejos_ y mi voz no te toca_
Vemos que las comas y puntos las ha sustituido por guiones bajos.
Bien, ya tenemos todos los ingredientes. Vamos a cocinar la receta:
texto = """Me gustas cuando callas porque estás como ausente,
y me oyes desde lejos, y mi voz no te toca."""
mapping = texto.maketrans(string.punctuation, "_" * len(string.punctuation))
print("-".join(texto.lower().split()).translate(mapping))
me_gustas_cuando_callas_porque_estás_como_ausente__y_me_oyes_desde_lejos__y_mi_voz_no_te_toca_
¿Qué ha pasado? Que el guión está entre los caracteres de puntuación y me los sustituye también. Podemos hacer varias cosas, o cambiar el orden de alguna operación o ignorar el guión en el mapeo. La segunda me parece más limpia y menos propensa a errores por lo que vamos con ella:
texto = """Me gustas cuando callas porque estás como ausente,
y me oyes desde lejos, y mi voz no te toca."""
mapping = texto.maketrans(
string.punctuation.replace("-", ""),
"_" * (len(string.punctuation) - 1)
)
print("-".join(texto.lower().split()).translate(mapping))
me-gustas-cuando-callas-porque-estás-como-ausente_-y-me-oyes-desde-lejos_-y-mi-voz-no-te-toca_
Genial. Lo hemos conseguido.
Podríamos, también, comprobar si nuestro texto empieza o termina por un texto usando los métodos startswith o endswith, respectivamente:
texto = "Pybonacci"
print(texto.startswith("Py"))
print(texto.endswith("ci"))
True
True
Podemos comprobar si el texto es numérico:
texto = "3.2"
print(texto.isalnum())
print(texto.isnumeric())
print(texto.isdecimal())
print(texto.isdigit())
texto = "32"
print(texto.isalnum())
print(texto.isnumeric())
print(texto.isdecimal())
print(texto.isdigit())
False
False
False
False
True
True
True
True
Existen otros métodos interesantes y os invito a que les echéis un ojo:
print(dir(texto))
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
Resumen y conclusiones
Hemos visto diferentes formas de formatear texto en Python y de trabajar con el mismo. Combinando diferentes metodologías de los que hemos visto y otras que se nos puedan ocurrir nos permitiría hacer cosas muy chulas.
¿Cuándo usar unas formas u otras de formateo?
Yo uso siempre los f-strings si estoy trabajando con una versión de Python >= 3.6. Normalmente no me llega texto de usuarios por lo que me evito el peligro de que ejecuten código potencialmente maligno. Si estás en una versión antigua usa format. Si te llega texto de usuarios prueba con string.Template para evitar problemas de seguridad (o puedes usar sistemas de plantillas como Jinja).
Algunas referencias:
- Formateando números en Pybonacci: https://pybonacci.org/2013/03/03/formateando-numeros/
- printf-style formatting: https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
- Literal string interpolation PEP: https://www.python.org/dev/peps/pep-0498/
- format string method PEP: https://www.python.org/dev/peps/pep-3101/
- PyFormat: https://pyformat.info/
- Template class en StackAbuse: https://stackabuse.com/formatting-strings-with-the-python-template-class/
Pybonacci