¿Cómo posponer / aplazar la evaluación de f-strings?

101

Estoy usando cadenas de plantilla para generar algunos archivos y me encanta la concisión de las nuevas cadenas f 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 de un archivo o algo. Esto significa que la plantilla es una cadena estática con etiquetas de formato. Algo tendría que sucederle a la cadena para decirle al intérprete que interprete la cadena como una nueva cadena f, pero no sé si existe tal cosa.

¿Hay alguna forma de introducir una cadena e interpretarla como una cadena f para evitar usar la .format(**locals())llamada?

Idealmente, quiero poder codificar así ... (donde magic_fstring_functiones donde entra 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}
JDAnders
fuente
5
No puedes hacer eso con una fcuerda. Una fcadena no es datos y ciertamente no es una cadena; es código. (Compruébelo con el dismódulo). Si desea que el código se evalúe más adelante, utilice una función.
algo así como
12
Para su información, PEP 501 propuso una función cercana a su primer ideal, pero actualmente está "diferida a la espera de más experiencia con [f-strings]".
jwodder
Una plantilla es una cadena estática, pero una f-cadena no es una cadena, es un objeto de código, como dijo @kindall. Creo que una f-string está vinculada a las variables inmediatamente cuando se crea una instancia (en Python 3.6,7), no cuando finalmente se usa. Así que f-string puede ser menos útil que tu feo viejo .format(**locals()), aunque cosméticamente más agradable. Hasta que se implemente PEP-501.
smci
Guido nos salvó, pero PEP 498 realmente lo arruinó . La evaluación diferida descrita por PEP 501 absolutamente debería haber sido incorporada a la implementación del núcleo f-string. Ahora nos quedamos regateando entre un str.format()método menos característico y extremadamente lento que admite la evaluación diferida por un lado y una sintaxis de cadena f más funcional y extremadamente rápida que no admite la evaluación diferida por el otro. Así que todavía necesitamos ambos y Python todavía no tiene un formateador de cadenas estándar. Inserte el meme de estándares xkcd.
Cecil Curry

Respuestas:

26

Aquí hay un "Ideal 2" completo.

No es una f-string, ni siquiera usa f-strings, pero hace lo solicitado. Sintaxis exactamente como se especifica. Sin dolores de cabeza de seguridad ya que no estamos usando eval().

Utiliza una pequeña clase e implementos __str__que se llama automáticamente por impresión. Para escapar del alcance limitado de la clase, usamos el inspectmódulo para saltar un fotograma 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
Paul Panzer
fuente
13
Voy a aceptar esto como la respuesta, aunque no creo que nunca lo use en el código debido a su extrema inteligencia. Bueno, tal vez nunca :). Tal vez la gente de Python pueda usarlo para la implementación de PEP 501 . Si mi pregunta fuera "¿cómo debo manejar este escenario?", La respuesta sería "seguir usando la función .format () y esperar a que se resuelva PEP 501". Gracias por descubrir cómo hacer lo que no debería hacerse, @PaulPanzer
JDAnders
6
Esto no funciona cuando la plantilla incluye algo más complejo que simples nombres de variables. Por ejemplo: template = "The beginning of the name is {name[:4]}"(-> TypeError: string indices must be integers)
bli
6
@bli Interesante, parece ser una limitación de str.format. Solía ​​pensar que las cuerdas f son solo azúcar sintáctico para algo así, str.format(**locals(), **globals())pero obviamente estaba equivocado.
Paul Panzer
4
Por favor, no lo use en producción. inspectes una bandera roja.
alexandernst
1
Tengo 2 preguntas, ¿por qué inspeccionar es una "señal de alerta" para la producción, un caso como este sería una excepción o habría soluciones alternativas más viables? ¿Y hay algo en contra del uso de __slots__aquí para el uso reducido de memoria?
Jab
21

Esto significa que la plantilla es una cadena estática con etiquetas de formato.

Sí, esa es exactamente la razón por la que tenemos literales con campos de reemplazo y .format, por lo tanto, podemos reemplazar los campos cuando queramos invocándolo format.

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

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

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

Que 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. Intentar usarlo en una situación que requiere nombres locales fallará miserablemente a menos que se pase a la cadena como argumentos (lo que supera totalmente el punto).

¿Hay alguna forma de introducir una cadena e interpretarla como una cadena f para evitar usar la .format(**locals())llamada?

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

Dimitris Fasarakis Hilliard
fuente
Es curioso, tenía exactamente el mismo fragmento publicado. Pero lo retiré debido a limitaciones de alcance. (Intente envolver el bucle for en una función.)
Paul Panzer
@PaulPanzer, ¿tal vez quieras editar la pregunta y volver a incluirla? No me importaría borrar la respuesta. Esta es una alternativa viable para el caso de OP. No es una alternativa viable para todos los casos, es engañosa.
Dimitris Fasarakis Hilliard
1
No, está bien, quédatelo. Estoy mucho más feliz con mi nueva solución. Pero puedo ver tu punto de que este es viable si eres consciente de sus limitaciones. ¿Quizás podrías agregar una pequeña advertencia a tu publicación para que nadie pueda disparar al usarla mal?
Paul Panzer
17

Una forma concisa de evaluar una cadena como una cadena f (con todas sus capacidades) es usar 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
Kadee
fuente
4
de lejos la mejor respuesta! ¿Cómo no incluyeron esta implementación simple como una característica incorporada cuando introdujeron f-strings?
user3204459
1
no, eso pierde alcance. la única razón por la que funciona es porque namees global. Las f-strings deben aplazarse en la evaluación, pero la clase FString necesita crear una lista de referencias a los argumentos de ámbito mirando a los llamadores locales y globales ... y luego evaluar la cadena cuando se usa.
Erik Aronesty
2
@ user3204459: Porque poder ejecutar cadenas arbitrarias es inherentemente un peligro de seguridad, por lo que eval()generalmente se desaconseja el uso de .
martineau
2
@martineau debería haber sido una característica de python para que no necesite usar eval ... además, f-string tiene los mismos riesgos que eval () ya que puede poner cualquier cosa entre corchetes, incluido el código malicioso, así que si eso es una preocupación, entonces no use f-strings
user3204459
2
Esto es exactamente lo que estaba buscando, agachándome para 'fstr pospone ". Eval no parece peor que el uso de fstrings en general, ya que, supongo, ambos poseen el mismo poder: f" {eval (' print (42) ')} "
user2692263
12

Una cadena f es simplemente una forma más concisa de crear una cadena formateada, reemplazando .format(**names)por f. Si no desea que una cadena se evalúe inmediatamente de esa manera, no la convierta en una cadena f. Guárdelo como un literal de cadena ordinario y luego invocalo formatmás tarde cuando desee realizar la interpolación, como lo ha estado haciendo.

Por supuesto, existe 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 todo lo que ha logrado hacer es reemplazar str.formatcon eval, lo que seguramente no vale la pena. Simplemente siga usando cadenas regulares con una formatllamada.

TigerhawkT3
fuente
3
Realmente no veo ninguna ventaja en tu fragmento de código. Quiero decir, siempre puedes escribir solo The current name is {name}dentro del template.txtarchivo y luego usar print(template_a.format(name=name))(o .format(**locals())). El código tiene aproximadamente 10 caracteres más, pero no presenta ningún problema de seguridad debido a eval.
Bakuriu
@Bakuriu - Sí; como dije, aunque evalnos permite escribir f'{name}'y retrasar la evaluación de namehasta que lo deseemos, es inferior a simplemente crear una cadena de plantilla regular y luego llamarla format, como ya estaba haciendo el OP.
TigerhawkT3
4
"Una cadena f es simplemente una forma más concisa de crear una cadena formateada, reemplazando .format (** nombres) con f". No exactamente, utilizan una sintaxis diferente. No tengo un python3 lo suficientemente reciente para verificar, pero, por ejemplo, creo que f '{a + b}' funciona, mientras que '{a + b}'. Format (a = a, b = b) genera KeyError . .format () probablemente esté bien en muchos contextos, pero no es un reemplazo directo.
philh
2
@philh Creo que sólo encontré un ejemplo donde .formatno es equivalente a un F-cadena, que puede soportar usted comenta: DNA = "TATTCGCGGAAAATATTTTGA"; fragment = f"{DNA[2:8]}"; failed_fragment = "{DNA[2:8]}".format(**locals()). El intento de crear failed_fragmentresultados en TypeError: string indices must be integers.
bli
12

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.

A continuación, se muestra un ejemplo de registrador diferido. Esto usa 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.

En mi humilde opinión: esta debería haber sido la operación predeterminada de f-strings, sin embargo, ahora es demasiado tarde . La evaluación de F-string puede tener efectos secundarios masivos y no deseados, y si eso sucede de manera diferida cambiará la ejecución del programa.

Para hacer que las f-strings se difieran correctamente, Python necesitaría alguna forma de cambiar explícitamente el comportamiento. ¿Quizás usar la letra 'g'? ;)

Se ha señalado que el registro diferido no debería fallar si hay un error en el convertidor de cadenas. La solución anterior también puede hacer esto, cambiar finally:a except:y pegar log.exceptionallí.

Erik Aronesty
fuente
1
De acuerdo con esta respuesta de todo corazón. Este caso de uso es en lo que estaba pensando al buscar esta pregunta.
mitad del
1
Esta es la respuesta correcta. Algunos horarios: %timeit log.finfo(f"{bar=}") 91.9 µs ± 7.45 µs per loop %timeit log.info(f"{bar=}") 56.2 µs ± 630 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each) log.setLevel(logging.CRITICAL) %timeit log.finfo("{bar=}") 575 ns ± 2.9 ns per loop %timeit log.info(f"{bar=}") 480 ns ± 9.37 ns per loop %timeit log.finfo("") 571 ns ± 2.66 ns per loop %timeit log.info(f"") 380 ns ± 0.92 ns per loop %timeit log.info("") 367 ns ± 1.65 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
Jaleks
8

Lo que desea parece estar considerado como una mejora de Python .

Mientras tanto, a partir de la discusión vinculada, lo siguiente parece ser una solución alternativa razonable que no requiere el uso eval():

class FL:
    def __init__(self, func):
        self.func = func
    def __str__(self):
        return self.func()


template_a = FL(lambda: f"The current name, number is {name!r}, {number+1}")
names = "foo", "bar"
numbers = 40, 41
for name, number in zip(names, numbers):
    print(template_a)

Salida:

The current name, number is 'foo', 41
The current name, number is 'bar', 42
martineau
fuente
7

inspirado en la respuesta de kadee , lo siguiente se puede usar para definir una clase de cadena f diferida.

class FStr:
    def __init__(self, s):
        self._s = s
    def __repr__(self):
        return eval(f"f'{self._s}'")

...

template_a = FStr('The current name is {name}')

names = ["foo", "bar"]
for name in names:
    print (template_a)

que es exactamente lo que pedía la pregunta

user3204459
fuente
4

O tal vez no use f-strings, solo formatee:

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))
msztolcman
fuente
Esto no funciona en todos los casos. Ejemplo: fun = "{DNA[2:8]}".format; DNA = "TATTCGCGGAAAATATTTTGA"; fun(DNA=DNA). ->TypeError: string indices must be integers
bli
Pero no funciona también en el uso normal, consulte la respuesta stackoverflow.com/questions/14072810/…
msztolcman
2

Qué tal si:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

s.format(foo='Bar')
> 'Hi, Bar!'
Denis
fuente
0

Una sugerencia que usa f-strings. Haga su evaluación en el nivel lógico donde se está produciendo la plantilla y páselo como un generador. Puede desenrollarlo en cualquier punto que elija, utilizando 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  
Ron Lawhorn
fuente