Formateo de cadenas con la clase de plantilla de Python

La clase Plantilla de Python está diseñada para usarse para la sustitución de cadenas o la interpolación de cadenas. La clase funciona con expresiones regulares y permite una personalización profunda

Introducción

Las plantillas de Python se utilizan para sustituir datos en cadenas. Con Plantillas, obtenemos una interfaz muy personalizable para la sustitución de cadenas (o interpolación de cadenas).

Python ya ofrece muchas formas de cuerdas sustitutas, incluyendo las recientemente introducidas Cuerdas f. Si bien es menos común sustituir cadenas con plantillas, su poder radica en cómo podemos personalizar nuestras reglas de formato de cadena.

En este artículo, daremos formato a las cadenas con la clase Template de Python. Luego veremos cómo podemos cambiar la forma en que nuestras Plantillas pueden sustituir datos en cadenas.

For a better understanding of these topics, you'll require some basic knowledge on how to work with clases and expresiones regulares.

Comprender la clase de plantilla de Python

La clase Template de Python se agregó al módulo string desde Python 2.4. Esta clase está pensada para usarse como una alternativa a las opciones de sustitución integradas (principalmente a %) para crear plantillas complejas basadas en cadenas y para manejarlas de una manera fácil de usar.

La implementación de la clase usa expresiones regulares para coincidir con un patrón general de cadenas de plantilla válidas. Una cadena de plantilla válida, o marcador de posición, consta de dos partes:

  • El símbolo $
  • Un identificador de Python válido. Un identificador es cualquier secuencia de letras mayúsculas y minúsculas de la A a la Z, guiones bajos (_) y dígitos del 0 al 9. Un identificador no puede comenzar con dígitos ni puede ser una palabra clave de Python.

En una cadena de plantilla, $nombre y $edad se considerarían marcadores de posición válidos.

Para usar la clase Template de Python en nuestro código, necesitamos:

  1. Importe Template desde el módulo string
  2. Cree una cadena de plantilla válida
  3. Crea una instancia de Template usando la cadena de plantilla como argumento
  4. Realice la sustitución usando un método de sustitución

Aquí hay un ejemplo básico de cómo podemos usar la clase Template de Python en nuestro código:

1
2
3
4
5
>>> from string import Template
>>> temp_str = 'Hi $name, welcome to $site'
>>> temp_obj = Template(temp_str)
>>> temp_obj.substitute(name='John Doe', site='wikihtp.com')
'Hi John Doe, welcome to wikihtp.com'

Notamos que cuando construimos la cadena de plantilla temp_str, usamos dos marcadores de posición: $name y $site. El signo $ realiza la sustitución real y los identificadores (nombre y sitio) se utilizan para asignar los marcadores de posición a los objetos concretos que necesitamos insertar en la cadena de plantilla.

La magia se completa cuando usamos el método sustituto() para realizar la sustitución y construir la cadena deseada. Piense en substitute() como si le estuviéramos diciendo a Python, revise esta cadena y si encuentra $name, reemplácela por John Doe. Continúe buscando a través de la cadena y, si encuentra el identificador $site, conviértalo en wikihtp.com.

Los nombres de los argumentos que pasamos a .substitute() deben coincidir con los identificadores que usamos en los marcadores de posición de nuestra cadena de plantilla.

La diferencia más importante entre Template y el resto de herramientas de sustitución de cadenas disponibles en Python es que no se tiene en cuenta el tipo de argumento. Podemos pasar cualquier tipo de objeto que se pueda convertir en una cadena Python válida. La clase Template convertirá automáticamente estos objetos en cadenas y luego los insertará en la cadena final.

Ahora que conocemos los conceptos básicos sobre cómo usar la clase Template de Python, profundicemos en los detalles de su implementación para comprender mejor cómo funciona la clase internamente. Con este conocimiento a la mano, podremos usar efectivamente la clase en nuestro código.

La cadena de plantilla

La cadena de plantilla es una cadena normal de Python que incluye marcadores de posición especiales. Como hemos visto antes, estos marcadores de posición se crean usando un signo $, junto con un identificador de Python válido. Una vez que tengamos una cadena de plantilla válida, los marcadores de posición se pueden reemplazar por nuestros propios valores para crear una cadena más elaborada.

Según PEP 292 -- Sustituciones de cadenas más simples, se aplican las siguientes reglas para el uso del signo $ en marcadores de posición :

  1. $$ es un escape; se reemplaza con un solo $
  2. $identificador nombra un marcador de posición de sustitución que coincide con una clave de mapeo de "identificador". De forma predeterminada, "identificador" debe escribir un identificador de Python como se define en http://docs.python.org/reference/lexical_analysis.html#identifiers-and-keywords. El primer carácter no identificador después del carácter $ finaliza esta especificación de marcador de posición.
  3. ${identificador} es equivalente a $identificador. Se requiere cuando los caracteres de identificación válidos siguen al marcador de posición pero no forman parte del marcador de posición, p. "${sust.}ificación". (Fuente)

Codifiquemos algunos ejemplos para comprender mejor cómo funcionan estas reglas.

Comenzaremos con un ejemplo de cómo podemos escapar del signo $. Supongamos que estamos tratando con monedas y necesitamos tener el signo de dólar en nuestras cadenas resultantes. Podemos duplicar el signo $ para escapar de sí mismo en la cadena de la plantilla de la siguiente manera:

1
2
3
>>> budget = Template('The $time budget for investment is $$$amount')
>>> budget.substitute(time='monthly', amount='1,000.00')
'The monthly budget for investment is $1,000.00'

Tenga en cuenta que no es necesario agregar espacio adicional entre el signo de escape y el siguiente marcador de posición como hicimos en $$$cantidad. Las plantillas son lo suficientemente inteligentes como para poder escapar del signo $ correctamente.

La segunda regla establece los conceptos básicos para crear un marcador de posición válido en nuestras cadenas de plantilla. Cada marcador de posición debe construirse con el carácter $ seguido de un identificador de Python válido. Echa un vistazo al siguiente ejemplo:

1
2
3
>>> template = Template('$what, $who!')
>>> template.substitute(what='Hello', who='World')
'Hello, World!'

Aquí, ambos marcadores de posición se forman utilizando identificadores de Python válidos (qué y quién). También tenga en cuenta que, como se indica en la segunda regla, el primer carácter no identificador finaliza el marcador de posición, como puede ver en $who!, donde el carácter ! no forma parte del marcador de posición, sino de la cadena final. .

Podría haber situaciones en las que necesitemos sustituir parcialmente una palabra en una cadena. Esa es la razón por la que tenemos una segunda opción para crear un marcador de posición. La tercera regla establece que ${identificador} es equivalente a $identificador y debe usarse cuando los caracteres de identificación válidos siguen al marcador de posición pero no son parte del marcador de posición en sí.

Supongamos que necesitamos automatizar la creación de archivos que contengan información comercial sobre los productos de nuestra empresa. Los archivos se nombran siguiendo un patrón que incluye el código del producto, el nombre y el lote de producción, todos ellos separados por un carácter de subrayado (_). Considere el siguiente ejemplo:

1
2
3
4
5
>>> filename_temp = Template('$code_$product_$batch.xlsx')
>>> filename_temp.substitute(code='001', product='Apple_Juice', batch='zx.001.2020')
Traceback (most recent call last):
  ...
KeyError: 'code_'

Dado que _ es un carácter identificador de Python válido, nuestra cadena de plantilla no funciona como se esperaba y Template genera un KeyError. Para corregir este problema, podemos usar la notación entre llaves (${identificador}) y construir nuestros marcadores de posición de la siguiente manera:

1
2
3
>>> filename_temp = Template('${code}_${product}_$batch.xlsx')
>>> filename_temp.substitute(code='001', product='Apple_Juice', batch='zx.001.2020')
'001_Apple_Juice_zx.001.2020.xlsx'

¡Ahora la plantilla funciona correctamente! Eso es porque las llaves separan correctamente nuestros identificadores del carácter _. Vale la pena señalar que solo necesitamos usar la notación entre llaves para código y producto y no para lote porque el carácter . que sigue a lote no es un carácter identificador válido en Python.

Finalmente, la cadena de la plantilla se almacena en la propiedad template de la instancia. Repasemos el ejemplo ¡Hola, mundo!, pero esta vez vamos a modificar un poco plantilla:

1
2
3
4
5
6
>>> template = Template('$what, $who!')  # Original template
>>> template.template = 'My $what, $who template'  # Modified template
>>> template.template
'My $what, $who template'
>>> template.substitute(what='Hello', who='World')
'My Hello, World template'

Dado que Python no restringe el acceso a los atributos de la instancia, podemos modificar nuestra cadena de plantilla para satisfacer nuestras necesidades cuando queramos. Sin embargo, esta no es una práctica común cuando se usa la clase Template de Python.

Es mejor crear nuevas instancias de Template para cada cadena de plantilla diferente que usamos en nuestro código. De esta manera, evitaremos algunos errores sutiles y difíciles de encontrar relacionados con el uso de cadenas de plantillas inciertas.

El método de sustitución()

Hasta ahora, hemos estado usando el método substitute() en una instancia de Template para realizar la sustitución de cadenas. Este método reemplaza los marcadores de posición en una cadena de plantilla usando argumentos de palabra clave o usando una asignación que contiene pares de identificador-valor.

Los argumentos de palabra clave o los identificadores en la asignación deben coincidir con los identificadores utilizados para definir los marcadores de posición en la cadena de plantilla. Los valores pueden ser de cualquier tipo de Python que se convierta correctamente en una cadena.

Ya que hemos cubierto el uso de argumentos de palabras clave en ejemplos anteriores, concentrémonos ahora en el uso de diccionarios. Aquí hay un ejemplo:

1
2
3
4
>>> template = Template('Hi $name, welcome to $site')
>>> mapping = {'name': 'John Doe', 'site': 'wikihtp.com'}
>>> template.substitute(**mapping)
'Hi John Doe, welcome to wikihtp.com'

Cuando usamos diccionarios como argumentos con substitute(), necesitamos usar el operador de desempaquetado del diccionario: ** . Este operador desempaquetará los pares clave-valor en argumentos de palabra clave que se utilizarán para sustituir los marcadores de posición coincidentes en la cadena de plantilla.

Errores comunes de plantilla

Hay algunos errores comunes que podemos introducir sin darnos cuenta cuando usamos la clase Template de Python.

Por ejemplo, se genera un KeyError cada vez que proporcionamos un conjunto incompleto de argumentos para substitute(). Considere el siguiente código que utiliza un conjunto incompleto de argumentos:

1
2
3
4
5
>>> template = Template('Hi $name, welcome to $site')
>>> template.substitute(name='Jane Doe')
Traceback (most recent call last):
  ...
KeyError: 'site'

Si llamamos a substitute() con un conjunto de argumentos que no coincide con todos los marcadores de posición en nuestra cadena de plantilla, entonces obtendremos un KeyError.

Si usamos un identificador de Python no válido en algunos de nuestros marcadores de posición, obtendremos un ValueError que nos indica que el marcador de posición es incorrecto.

Tome este ejemplo en el que usamos un identificador no válido, $0name como marcador de posición en lugar de $name.

1
2
3
4
5
>>> template = Template('Hi $0name, welcome to $site')
>>> template.substitute(name='Jane Doe', site='wikihtp.com')
Traceback (most recent call last):
  ...
ValueError: Invalid placeholder in string: line 1, col 4

Solo cuando el objeto Template lee la cadena de la plantilla para realizar la sustitución, descubre el identificador no válido. Inmediatamente genera un ValueError. Tenga en cuenta que 0name no es un identificador o nombre de Python válido porque comienza con un dígito.

El método safe_substitute()

La clase Template de Python tiene un segundo método que podemos usar para realizar la sustitución de cadenas. El método se llama safe_substitute(). Funciona de manera similar a substitute() pero cuando usamos un conjunto de argumentos incompletos o que no coinciden, el método no genera un KeyError.

En este caso, el marcador de posición que falta o no coincide aparece sin cambios en la cadena final.

Así es como funciona safe_substitute() usando un conjunto incompleto de argumentos (faltará site):

1
2
3
>>> template = Template('Hi $name, welcome to $site')
>>> template.safe_substitute(name='John Doe')
'Hi John Doe, welcome to $site'

Aquí, primero llamamos a safe_substitute() usando un conjunto incompleto de argumentos. La cadena resultante contiene el marcador de posición original $sitio, pero no se genera ningún KeyError.

Personalización de la clase de plantilla de Python

La clase Template de Python está diseñada para la creación de subclases y la personalización. Esto nos permite modificar los patrones de expresiones regulares y otros atributos de la clase para satisfacer nuestras necesidades específicas.

En esta sección, cubriremos cómo personalizar algunos de los atributos más importantes de la clase y cómo esto afecta el comportamiento general de nuestros objetos Template. Comencemos con el atributo de clase .delimiter.

Uso de un delimitador diferente {#usinga differentdelimiter}

El atributo de clase delimitador contiene el carácter utilizado como carácter inicial del marcador de posición. Como hemos visto hasta ahora, su valor predeterminado es $.

Dado que la clase Template de Python está diseñada para la herencia, podemos subclasificar Template y cambiar el valor predeterminado de delimiter anulándolo. Eche un vistazo al siguiente ejemplo donde anulamos el delimitador para usar # en lugar de $:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from string import Template
class MyTemplate(Template):
    delimiter = '#'

template = MyTemplate('Hi #name, welcome to #site')
print(template.substitute(name='Jane Doe', site='wikihtp.com'))

# Output:
# 'Hi Jane Doe, welcome to wikihtp.com'

# Escape operations also work
tag = MyTemplate('This is a Twitter hashtag: ###hashtag')
print(tag.substitute(hashtag='Python'))

# Output:
# 'This is a Twitter hashtag: #Python'

Podemos usar nuestra clase MyTemplate tal como usamos la clase Template normal de Python. Sin embargo, ahora debemos usar # en lugar de $ para construir nuestros marcadores de posición. Esto puede ser útil cuando estamos trabajando con cadenas que manejan muchos signos de dólar, por ejemplo, cuando estamos tratando con monedas.

Nota: No reemplace un delimitador con una expresión regular. La clase de plantilla escapa automáticamente del delimitador. Por lo tanto, si usamos una expresión regular como ‘delimitador’, es muy probable que nuestra ‘Plantilla’ personalizada no funcione correctamente.

Cambiar lo que califica como un identificador {#cambiar lo que califica como un identificador}

El atributo de clase idpattern contiene una expresión regular que se usa para validar la segunda mitad de un marcador de posición en una cadena de plantilla. En otras palabras, idpattern valida que los identificadores que usamos en nuestros marcadores de posición sean identificadores de Python válidos. El valor predeterminado de idpattern es r'(?-i:[_a-zA-Z][_a-zA-Z0-9]*)'.

Podemos subclasificar Template y usar nuestro propio patrón de expresión regular para idpattern. Supongamos que necesitamos restringir los identificadores a nombres que no contengan guiones bajos (_) ni dígitos ([0-9]). Para hacer esto, podemos anular idpattern y eliminar estos caracteres del patrón de la siguiente manera:

1
2
3
4
5
6
7
from string import Template
class MyTemplate(Template):
    idpattern = r'(?-i:[a-zA-Z][a-zA-Z]*)'

# Underscores are not allowed
template = MyTemplate('$name_underscore not allowed')
print(template.substitute(name_underscore='Jane Doe'))

Si ejecutamos este código obtendremos este error:

1
2
3
Traceback (most recent call last):
    ...
KeyError: 'name'

Podemos confirmar que los dígitos tampoco están permitidos:

1
2
template = MyTemplate('$python3 digits not allowed')
print(template.substitute(python3='Python version 3.x'))

El error será:

1
2
3
Traceback (most recent call last):
    ...
KeyError: 'python'

Dado que el guión bajo y los dígitos no están incluidos en nuestro idpattern personalizado, el objeto Template aplica la segunda regla y rompe el marcador de posición con el primer carácter no identificador después de $. Es por eso que obtenemos un KeyError en cada caso.

Creación de subclases de plantillas avanzadas

Puede haber situaciones en las que necesitemos modificar el comportamiento de la clase Template de Python, pero anular delimiter, idpattern o ambos no es suficiente. En estos casos, podemos ir más allá y anular el atributo de clase patrón para definir una expresión regular completamente nueva para nuestras subclases Plantilla personalizadas.

Si decide utilizar una expresión regular completamente nueva para patrón, debe proporcionar una expresión regular con cuatro grupos con nombre:

  1. escaped coincide con la secuencia de escape del delimitador, como en $$
  2. named coincide con el delimitador y un identificador de Python válido, como en $identificador
  3. braced coincide con el delimitador y un identificador válido de Python usando llaves, como en ${identifier}
  4. inválido coincide con otros delimitadores mal formados, como en $0sitio

La propiedad patrón contiene un objeto de expresión regular compilado. Sin embargo, es posible inspeccionar la cadena de expresión regular original accediendo al atributo patrón de la propiedad patrón. Mira el siguiente código:

1
2
3
4
5
6
7
8
>>> template = Template('$name')
>>> print(template.pattern.pattern)
\$(?:
    (?P<escaped>\$) |   # Escape sequence of two delimiters
    (?P<named>(?-i:[_a-zA-Z][_a-zA-Z0-9]*))      |   # delimiter and a Python identifier
    {(?P<braced>(?-i:[_a-zA-Z][_a-zA-Z0-9]*))}   |   # delimiter and a braced identifier
    (?P<invalid>)              # Other ill-formed delimiter exprs
  )

Este código genera la cadena predeterminada utilizada para compilar el atributo de clase patrón. En este caso, podemos ver claramente los cuatro grupos con nombre que se ajustan a la expresión regular predeterminada. Como se indicó anteriormente, si necesitamos personalizar profundamente el comportamiento de Template, entonces debemos proporcionar estos mismos cuatro grupos con nombre junto con expresiones regulares específicas para cada grupo.

Ejecutar código con eval() y exec()

Nota: Las funciones integradas eval() y exec() pueden tener importantes implicaciones de seguridad cuando se usan con entradas maliciosas. ¡Usar con precaución!

Esta última sección pretende abrirle los ojos sobre cuán poderosa puede ser la clase Template de Python si la usamos junto con algunas funciones integradas de Python como eval() y exec().

La función eval() ejecuta una única expresión de Python y devuelve su resultado. La función exec() también ejecuta una expresión de Python, pero nunca devuelve su valor. Normalmente usa exec() cuando solo está interesado en el efecto secundario de una expresión, como un valor de variable modificado, por ejemplo.

Los ejemplos que vamos a cubrir pueden parecer algo poco convencionales, pero estamos seguros de que puede encontrar algunos casos de uso interesantes para esta poderosa combinación de herramientas de Python. ¡Dan una idea de cómo funcionan las herramientas que generan código Python!

Para el primer ejemplo, vamos a usar una Plantilla junto con eval() para crear listas dinámicamente a través de una lista de comprensión:

1
2
3
4
5
6
7
8
>>> template = Template('[$exp for item in $coll]')
>>> eval(template.substitute(exp='item ** 2', coll='[1, 2, 3, 4]'))
[1, 4, 9, 16]
>>> eval(template.substitute(exp='2 ** item', coll='[3, 4, 5, 6, 7, 8]'))
[8, 16, 32, 64, 128, 256]
>>> import math
>>> eval(template.substitute(expression='math.sqrt(item)', collection='[9, 16, 25]'))
[3.0, 4.0, 5.0]

Nuestro objeto de plantilla en este ejemplo contiene la sintaxis básica de una lista de comprensión. A partir de esta plantilla, podemos crear listas dinámicamente sustituyendo los marcadores de posición con expresiones válidas (exp) y colecciones (coll). Como paso final, ejecutamos la comprensión usando eval().

Dado que no hay límite en la complejidad de nuestras cadenas de plantilla, es posible crear cadenas de plantilla que contengan cualquier pieza de código de Python. Consideremos el siguiente ejemplo de cómo usar un objeto Template para crear una clase completa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from string import Template

_class_template = """
class ${klass}:
    def __init__(self, name):
        self.name = name

    def ${method}(self):
        print('Hi', self.name + ',', 'welcome to', '$site')
"""

template = Template(_class_template)
exec(template.substitute(klass='MyClass',
                         method='greet',
                         site='wikihtp.com'))

obj = MyClass("John Doe")
obj.greet()

Aquí, creamos una cadena de plantilla para contener una clase de Python completamente funcional. Luego podemos usar esta plantilla para crear diferentes clases pero usando diferentes nombres de acuerdo a nuestras necesidades.

En este caso, exec() crea la clase real y la trae a nuestro espacio de nombres actual. A partir de este momento, podemos usar libremente la clase como lo haríamos con cualquier clase normal de Python.

Aunque estos ejemplos son bastante básicos, muestran cuán poderosa puede ser la clase Template de Python y cómo podemos aprovecharla para resolver problemas de programación complejos en Python.

Conclusión

La clase Template de Python está destinada a ser utilizada para la sustitución de cadenas o la interpolación de cadenas. La clase funciona con expresiones regulares y proporciona una interfaz poderosa y fácil de usar. Es una alternativa viable a otras opciones de sustitución de cadenas integradas cuando se trata de crear plantillas complejas basadas en cadenas.

En este artículo, hemos aprendido cómo funciona la clase Template de Python. También aprendimos sobre los errores más comunes que podemos introducir cuando usamos Template y cómo solucionarlos. Finalmente, cubrimos cómo personalizar la clase a través de subclases y cómo usarla para ejecutar código de Python.

Con este conocimiento a la mano, estamos en mejores condiciones para usar efectivamente la clase Template de Python para realizar la interpolación o sustitución de cadenas en nuestro código. .

Licensed under CC BY-NC-SA 4.0