Formando cadenas de caracteres



Construir una única cadena de caracteres a partir de otras más pequeñas es algo que estarás haciendo practicamente siempre al momento de escribir código, sea en Python o en cualquier otro lenguaje. Por esta razón, tener un buen dominio de las herramientas para llevarlo a cabo es fundamental para diseñar un mejor código: más eficiente, más legible. En este artículo intentaré transmitirle al lector los diversos métodos del formateo de cadenas y, por supuesto, recomendarte la manera «estándar» o correcta de hacerlo en Python.

Comencemos por observar una de las formas más básicas de concatenar dos cadenas.

>>> nombre = "Juan"
>>> "Tu nombre es: " + nombre
'Tu nombre es: Juan'

El operador de suma, cuando es utilizado entre dos cadenas, opera añadiendo la segunda inmediatamente luego de la primera. Generalmente este método es correcto y eficiente para pequeñas uniones como la anterior, pero se torna un tanto molesto cuando intentamos conformar una cadena a partir de numerosos fragmentos.

>>> apellido = "Pérez"
>>> sexo = "Masculino"
>>> "Tu nombre es: " + nombre + ". Apellido: " + apellido + ". Sexo: " + sexo + "."
'Tu nombre es: Juan. Apellido: Pérez. Sexo: Masculino.'

Incluso deberemos convertir (utilizando la función str) cualquier objeto que no sea una cadena para que la operación se concrete; por ejemplo, un entero.

>>> edad = 20
>>> "Tu nombre es " + nombre + " y tienes " + edad + " años."
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly
>>> "Tu nombre es " + nombre + " y tienes " + str(edad) + " años."
'Tu nombre es Juany tienes 20 años.'

Ésta es probablemente la forma en que generamos cadenas a partir de múltiples datos cuando comenzamos con el lenguaje o bien aun no conocemos otros métodos que son mucho más ventajosos y legibles. Si es tu caso, te invito a seguir leyendo.

El método original

La forma en la que originalmente Python reemplazaba el método anterior es similar a las funciones printf, scanf, etc. de C y funciona de la siguiente manera. En lugar de concatenar múltiples datos utilizando el operador de suma, creamos una única cadena y utilizamos el caracter % para indicar que en aquél lugar se insertará un dato. Siguiendo el ejemplo anteiror:

>>> "Tu nombre es %s" % nombre
'Tu nombre es Juan'

Como ves, «%s» fue reemplazado por «Juan». La letra después del caracter % indica el tipo de valor que vamos a ingresar (en este caso, una cadena). %s denota una cadena, %d un entero (de base 10), %f un número de coma flotante, entre otros que veremos más adelante.

>>> "Tienes %d años." % edad
'Tienes 20 años.'

Cuando indicamos que queremos incluir más de un valor (es decir tenemos varios «%» dentro de nuestra cadena) entonces usamos una tupla.

>>> altura = 1.75
>>> "Tu nombre es %s. Tienes %d años. Mides %f metros." % (nombre, edad, altura)
'Tu nombre es Juan. Tienes 20 años. Mides 1.750000 metros.'

¿Qué hacen esos ceros de más en la altura? Bueno, es algo que vamos a ver en un ratito.

Me falta decir que si quieres incluir el caracter «%» literalmente (por ejemplo, para denotar un porcentaje) entonces tienes que ubicarlo dos veces.

>>> probabilidad = 70
>>> "Hay una probabilidad del %d%%" % probabilidad
'Hay una probabilidad del 70%'

Cuando utilices %s pero pases un valor que no sea una cadena, será convertido automáticamente utilizando la función str. Esto puede tener consecuencias no deseadas por lo que siempre es conveniente precisar el tipo de dato que vamos a incluir.

# Poco recomendado.
>>> "Tienes %s años. Mides %s metros." % (edad, altura)
'Tienes 20 años. Mides 1.75 metros.'

Es equivalente a:

>>> "Tienes %s años. Mides %s metros." % (str(edad), str(altura))
'Tienes 20 años. Mides 1.75 metros.'

Finalizando con este método, podemos ver que es mucho más legible que el anterior (concatenar utilizando el operador de suma). Pero incluye otras ventajas. Manejar una única cadena en lugar de varias es una de ellas. Imaginate tener que desarrollar un proyecto que soporte múltiples idiomas, utilizando un motor de traducción o internacionalización, sería imposible tener que traducir textos por partes ya que cada una de ellas depende exclusivamente del contexto en el que esté ubicada.

No obstante, aun no hemos dicho nada de una de las funcionalidades más importantes de este sistema de formateo. Es mucho más que un simple método que «reemplaza el caracter % por un valor determinado». Permite alterar dichos valores y realizar conversiones a partir de un pequeño conjunto de reglas. Por ejemplo, para números de coma flotante, este sistema de formateo nos permite indicar la cantidad de decimales que queremos mostrar.

>>> a = 14.54378
>>> "a es %.2f" % a
'a es 14.54'

En el caso de los valores de coma flotante, se coloca un punto y la cantidad de decimales deseados a continuación. Si dicha cantidad excede el número de decimales luego de la coma, entonces se añaden ceros.

>>> altura = 1.75
>>> "Mides %.6f metros." % altura
'Mides 1.750000 metros.'

6 es el número de decimales por defecto que la instrucción %f incluye, y es justamente lo que sucedió anteriormente.

Para valores numéricos, añadiendo un signo de suma + producirá que los valores positivos incluyan dicho signo.

>>> for i in range(-3, 4):
...     print("%+d" % i)
...
-3
-2
-1
+0
+1
+2
+3

Para imprimir números hexadecimales puede utilizarse el indicador %x (que mostrará las letras en minúscula) o bien %X (en mayúscula). A su vez este indicador acepta otras especficiaciones. Por ejemplo, añadiendo # (signo numeral) prefijará «0x» o bien «0X».

>>> "%x" % 10
'a'
>>> "%#x" % 10
'0xa'

Como yo prefiero «0x» con las letras en mayúscula, me inclino por hacerlo manualmente:

>>> "0x%X" % 10
'0xA'

Por último, es bastante útil especificar el largo del valor que queremos imprimir. Te será de mucha ayuda cuando quieras alinear múltiples datos.

>>> for nombre in ("Juan", "Pedro", "Francisco"):
...     print("%9s" % nombre)
...
     Juan
    Pedro
Francisco

Antes de especificar el tipo de dato (una cadena en este caso) se ubica la cantidad de caracteres mínimos. Si la cadena tiene menor cantidad, entonces se agregan espacios. También opera con números.

>>> for n in (1, 10, 100):
...     print("%3d" % n)
...
  1
 10
100

O bien, para añadir ceros:

>>> for n in (1, 10, 100):
...     print("%03d" % n)
...
001
010
100

Puedes terminar de conocer todas las opciones de este sistema de formateo en este enlace.

El nuevo método (y recomendado)

Si bien el sistema anterior es completamente soportado por Python, el lenguaje, a partir de su versión 2.6, incluye un nuevo método que pretende ser el estándar y que deberías estar usando. Es bastante simple y similar al anterior.

En lugar de utilizar el operador % se llama a la función format que es un método de la clase str (unicode en Python 2).

>>> nombre = "Juan"
>>> "Tu nombre es {0}".format(nombre)
'Tu nombre es Juan'

Como ves, en lugar de posicionar el operador % seguido del tipo de valor, simplemente se colocan llaves y no es necesario indicar el tipo de dato (además, el método anterior solo soportaba tres tipos de datos: cadenas, números enteros y de coma flotante). Los datos que queremos incluir dentro de la cadena se pasan como argumentos a la función format. El número dentro de las llaves indica la posición del argumento que será reemplazado en aquel lugar. Como nombre es el primer argumento, empezando a contar desde el cero, entonces será ubicado en donde se encuentra {0}.

>>> edad = 20
>>> "Tu nombre es {0} y tienes {1} años.".format(nombre, edad)
'Tu nombre es Juan y tienes 20 años.'

Esto resulta bastante práctico ya que podemos alterar la cadena sin necesidad de cambiar el orden de los argumentos.

>>> "Tienes {1} años y te llamas {0}.".format(nombre, edad)
'Tienes 20 años y te llamas Juan.'

Incluso puedes colocar las llaves tantas veces como quieras sin necesidad de repetir los argumentos, lo que hubiese sido una limitación en el sistema anterior.

>>> "{0} {1} {0} {1} {1} {0} {0}".format(nombre, edad)
Juan 20 Juan 20 20 Juan Juan'

Si te resulta más cómodo, a partir de Python 2.7 puedes omitir los números dentro las llaves.

>>> "Tu nombre es {} y tienes {} años.".format(nombre, edad)
'Tu nombre es Juan y tienes 20 años.'

Aunque lógicamente, omitiendo las posiciones no te permitirá realizar repeticiones, pero no será necesario en la mayoría de los casos.

Otra caracterísitca que añade este nuevo sistema es la posibilidad de especificar con un nombre determinado los valores que queremos incluir.

>>> "Tu nombre es {a} y tienes {b} años.".format(a=nombre, b=edad)
'Tu nombre es Juan y tienes 20 años.'

Si bien en el ejemplo puede resultar algo trivial, puede serte de utilidad para cadenas más complejas. No hay problema en combinar ambos métodos.

>>> "Tu nombre es {0} y tienes {1} años. Mides {altura} metros.".format(nombre, edad, altura=1.75)
'Tu nombre es Juan y tienes 20 años. Mides 1.75 metros.'

Este sistema de formateo incluye las mismas herramientas que describimos anteriormente. Simplemente añadimos dos puntos y especificamos los diversos parámetros. Por ejemplo:

>>> for i in range(-3, 4):
...     print("{0:+}".format(i))
...
-3
-2
-1
+0
+1
+2
+3

Existen algunas diferencias. Por ejemplo, al indicar la cantidad de caracteres que queremos imprimir como mínimo, este sistema por defecto utilizará una alineación izquierda, mientras que el anterior derecha. Para cambiar este comportamiento utilizamos los caracteres «<" (izquierda) y ">» (derecha).

>>> for nombre in ("Juan", "Pedro", "Francisco"):
...     print("{0:>9}".format(nombre))
...
     Juan
    Pedro
Francisco

Empleando el caracter «^» causará que los nombres se centren.

>>> for nombre in ("Juan", "Pedro", "Francisco"):
...     print("{0:^9}".format(nombre))
...
  Juan
  Pedro
Francisco

Para convertir a hexadecimal:

>>> "0x{:X}".format(1000)  # Omitimos el 0.
'0x3E8'

Nótese que los valores que se colocan luego de los dos puntos también pueden ser «formateados». Por ejemplo, el siguiente código imprime el número 10 pero establece que como mínimo debe contener 5 caracteres.

>>> "{:5}".format(10)
'   10'

Es equivalente a:

>>> "{:{}}".format(10, 5)
'   10'

Al igual que en el método anterior, para imprimir literalmente llaves, deben colocarse dos veces.

>>> "{{}}".format()
'{}'

Este sistema de formateo estándar de Python está explicado íntegramente en el documento PEP 3101 — Advanced String Formatting. Puedes consultar el resto de las funcionalidades allí.

Antes de llegar a la última parte del artículo dejo algunos ejemplos.

El siguiente código crea una suerte de pirámide a partir de un número determinado de caracteres.

n = 70
for i in range(1, n, 2):
    print("  {0:^{1}}".format("A" * i, n))

Puedes correrlo y ver qué pasa.

Este otro código representa una tabla con información sobre los lenguajes más populares del 2016 según TIOBE.

ranking = (
    (1, 1, "", "Java", 18.236, -1.33),
    (2, 2, "", "C", 10.955, -4.67),
    (3, 3, "", "C++", 6.657, -0.13),
    (4, 4, "", "C#", 5.493, +0.58),
    (5, 5, "", "Python", 4.302, +0.64),
    (6, 7, "^", "JavaScript", 2.929, +0.59),
    (7, 6, "v", "PHP", 2.847, +0.32),
    (8, 11, "^", "Assembly language", 2.417, +0.61),
    (9, 8, "v", "Visual Basic .NET", 2.343, +0.28),
    (10, 9, "v", "Perl", 2.333, +0.43),
    (11, 13, "^", "Delphi/Object Pascal", 2.169, +0.42),
    (12, 12, "", "Ruby", 1.965, +0.18),
    (13, 16, "^", "Swift", 1.930, +0.74),
    (14, 10, "vv", "Objective-C", 1.849, +0.03),
    (15, 17, "^", "MATLAB", 1.826, +0.65),
    (16, 34, "^^", "Groovy", 1.818, +1.31),
    (17, 14, "v", "Visual Basic", 1.761, +0.23),
    (18, 19, "^", "R", 1.684, +0.64),
    (19, 44, "^^", "Go", 1.625, +1.37),
    (20, 18, "v", "PL/SQL", 1.443, +0.36)
)

table = """\
+---------------------------------------------------------------------+
| Sep 2016 Sep 2015 Change Language               Ratings (%)  Change |
|---------------------------------------------------------------------|
{}
+---------------------------------------------------------------------+\
"""
table = table.format("\n".join(
    "| {:<8} {:<8} {:<6} {:22} {:<11} {:+6}  |".format(*row)
    for row in ranking))
print(table)

El resultado es el siguiente.

Vista previa

El objetivo es ilustrar el parámetro de longitud de los valores para alinearlos con sus respectivas columnas.

Un nuevo método en Python 3.6

Con la idea de mejorar la legibilidad respecto de los dos sistemas anteriores, en Python 3.6 surgen las denominadas «f-strings» o «cadenas-f». Cuando una cadena esté precedida por una «f», utilizando llaves se podrán ubicar expresiones dentro de la misma que serán luego ejecutadas.

>>> nombre = "Juan"
>>> edad = 20
>>> altura = 1.75
>>> f"Te llamas {nombre}, tienes {edad} años y mides {altura} metros."
'Te llamas Juan, tienes 20 años y mides 1.75 metros.'

Nótese que son expresiones, no simples identificadores, a diferencia de los métodos anteriores.

>>> f"Tu nombre al revés es {nombre[::-1]}."
'Tu nombre al revés es nauJ.'
>>> f"Dentro de 5 años tendrás {edad + 5} años."
'Dentro de 5 años tendrás 25 años.'

En veriones anteriores, utilizando str.format se intentaba emular este funcionamiento vía el siguiente código (que en lo peronsal me disgusta).

>>> "Tu nombre es {nombre} y tienes {edad} años.".format(**locals())
'Tu nombre es Juan y tienes 20 años.'

Ya que locals() retorna un diccionario con los nombres de los objetos y sus respectivos valores.

Volviendo al nuevo sistema, también incluye las características de str.format con la misma sintaxis.

>>> for nombre in ("Juan", "Pedro", "Francisco"):
...     print(f"{nombre:>9}")
...
     Juan
    Pedro
Francisco

Las «cadenas-f» se encuentran documentadas en PEP 498 — Literal String Interpolation.

Comentarios finales

Hemos hecho un recorrido por los distintos sistemas de formateo que provee el lenguaje. Tener un buen conocimiento sobre estos le dará mayor legibilidad y potencial a tu código en algo tan cotidiano como lo es formar cadenas a partir de dos o múltiples datos. Como dije anteriormente, el sistema vía str.format es preferible. A la fecha en la que escribo este artículo, Python 3.6 se encuentra en estado beta, pero probablemente las «cadenas-f» se conviertan en el estándar a partir de dicha versión (y con razón, porque son muy cómodas), ¡así que ya estás al tanto de cómo funcionan!



3 comentarios.

Deja un comentario