¿Puedo posponer / diferir la evaluación de f-strings?

Estoy usando cadenas de plantilla para generar algunos archivos y me encanta la concisión de las nuevas cadenas de caracteres para este propósito, para reducir mi código de plantilla anterior de algo como esto:

template_a = "The current name is {name}" names = ["foo", "bar"] for name in names: print (template_a.format(**locals())) 

Ahora puedo hacer esto, reemplazando directamente las variables:

 names = ["foo", "bar"] for name in names: print (f"The current name is {name}") 

Sin embargo, a veces tiene sentido tener la plantilla definida en otro lugar, más arriba en el código, o importada desde un archivo o algo. Esto significa que la plantilla es una cadena estática con tags de formato en ella . Algo debería sucederle a la cadena para decirle al intérprete que interprete la cadena como una nueva cadena de caracteres, pero no sé si existe tal cosa.

¿Hay alguna forma de introducir una cadena y hacer que se interprete como una cadena f para evitar el uso de la .format(**locals()) ?

Idealmente, quiero poder codificar así … (donde magic_fstring_function es donde magic_fstring_function la parte que no entiendo):

 template_a = f"The current name is {name}" # OR [Ideal2] template_a = magic_fstring_function(open('template.txt').read()) names = ["foo", "bar"] for name in names: print (template_a) 

… con esta salida deseada (sin leer el archivo dos veces):

 The current name is foo The current name is bar 

… pero la salida real que obtengo es:

 The current name is {name} The current name is {name} 

Aquí hay un completo “Ideal 2”.

No es un f-string que ni siquiera usa f-strings. Pero hace lo solicitado. Sintaxis exactamente como se especifica. No hay dolores de cabeza de seguridad ya que no estamos usando eval.

Utiliza una pequeña clase e implementa __str__ que se llama automáticamente por impresión. Para escapar del scope limitado de la clase, usamos el módulo de inspect para saltar un cuadro y ver las variables a las que tiene acceso la persona que llama.

 import inspect class magic_fstring_function: def __init__(self, payload): self.payload = payload def __str__(self): vars = inspect.currentframe().f_back.f_globals.copy() vars.update(inspect.currentframe().f_back.f_locals) return self.payload.format(**vars) template = "The current name is {name}" template_a = magic_fstring_function(template) # use it inside a function to demonstrate it gets the scoping right def new_scope(): names = ["foo", "bar"] for name in names: print(template_a) new_scope() # The current name is foo # The current name is bar 

Una f-string es simplemente una manera más concisa de crear una cadena formateada, reemplazando .format(**names) con f . Si no desea que una cadena se evalúe de manera inmediata, no la convierta en una cadena de caracteres. Guárdelo como un literal de cadena normal, y luego llame al format más tarde cuando desee realizar la interpolación, como lo ha estado haciendo.

Por supuesto, hay una alternativa con eval .

template.txt :

f’El nombre actual es {nombre} ‘

Código:

 >>> template_a = open('template.txt').read() >>> names = 'foo', 'bar' >>> for name in names: ... print(eval(template_a)) ... The current name is foo The current name is bar 

Pero luego, todo lo que ha logrado hacer es reemplazar str.format con eval , que seguramente no vale la pena. Simplemente siga utilizando cadenas regulares con una llamada de format .

Esto significa que la plantilla es una cadena estática con tags de formato en ella

Sí, esa es exactamente la razón por la que tenemos literales con campos de reemplazo y .format , por lo que podemos reemplazar los campos siempre que lo .format llamando al format .

Algo debería sucederle a la cadena para decirle al intérprete que interprete la cadena como una nueva cadena F

Ese es el prefijo f/F Podría envolverlo en una función y posponer la evaluación durante el tiempo de la llamada, pero por supuesto eso implica una sobrecarga adicional:

 template_a = lambda: f"The current name is {name}" names = ["foo", "bar"] for name in names: print (template_a()) 

Que se imprime:

 The current name is foo The current name is bar 

pero se siente mal y está limitado por el hecho de que solo puede echar un vistazo al espacio de nombres global en sus reemplazos. Tratar de usarlo en una situación que requiere nombres locales fallará miserablemente a menos que se pase a la cadena como argumentos (que supera totalmente el punto).

¿Hay alguna forma de introducir una cadena y hacer que se interprete como una cadena f para evitar el uso de la .format(**locals()) ?

Aparte de una función (limitaciones incluidas), no, así que también podría seguir con .format .

O tal vez no use f-strings, solo el formato:

 fun = "The curent name is {name}".format names = ["foo", "bar"] for name in names: print(fun(name=name)) 

En versión sin nombres:

 fun = "The curent name is {}".format names = ["foo", "bar"] for name in names: print(fun(name)) 

Una forma concisa de evaluar una cadena como una cadena f (con todas sus capacidades) es mediante la siguiente función:

 def fstr(template): return eval(f"f'{template}'") 

Entonces puedes hacer:

 template_a = "The current name is {name}" names = ["foo", "bar"] for name in names: print(fstr(template_a)) # The current name is foo # The current name is bar 

Y, a diferencia de muchas otras soluciones propuestas, también puede hacer:

 template_b = "The current name is {name.upper() * 2}" for name in names: print(fstr(template_b)) # The current name is FOOFOO # The current name is BARBAR 

Usar .format no es una respuesta correcta a esta pregunta. Las cadenas F de Python son muy diferentes de las plantillas str.format () … pueden contener código u otras operaciones costosas, de ahí la necesidad de aplazamiento.

Aquí hay un ejemplo de un registrador diferido. Esto utiliza el preámbulo normal de logging.getLogger, pero luego agrega nuevas funciones que interpretan la cadena f solo si el nivel de registro es correcto.

 log = logging.getLogger(__name__) def __deferred_flog(log, fstr, level, *args): if log.isEnabledFor(level): import inspect frame = inspect.currentframe().f_back.f_back try: fstr = 'f"' + fstr + '"' log.log(level, eval(fstr, frame.f_globals, frame.f_locals)) finally: del frame log.fdebug = lambda fstr, *args: __deferred_flog(log, fstr, logging.DEBUG, *args) log.finfo = lambda fstr, *args: __deferred_flog(log, fstr, logging.INFO, *args) 

Esto tiene la ventaja de poder hacer cosas como: log.fdebug("{obj.dump()") …. sin volcar el objeto a menos que la depuración esté habilitada.

Una sugerencia que utiliza f-cuerdas. Haga su evaluación en el nivel lógico donde se producen las plantillas y pásela como un generador. Puedes desenrollarlo en el punto que elijas, usando f-strings

 In [46]: names = (i for i in ('The CIO, Reed', 'The homeless guy, Arnot', 'The security guard Spencer')) In [47]: po = (f'Strangely, {next(names)} has a nice {i}' for i in (" nice house", " fast car", " big boat")) In [48]: while True: ...: try: ...: print(next(po)) ...: except StopIteration: ...: break ...: Strangely, The CIO, Reed has a nice nice house Strangely, The homeless guy, Arnot has a nice fast car Strangely, The security guard Spencer has a nice big boat