Come posticipare / differire la valutazione delle stringhe f?


101

Sto usando stringhe modello per generare alcuni file e adoro la concisione delle nuove stringhe f per questo scopo, per ridurre il mio codice modello precedente da qualcosa di simile:

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

Ora posso farlo, sostituendo direttamente le variabili:

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

Tuttavia, a volte ha senso avere il modello definito altrove, più in alto nel codice o importato da un file o qualcosa del genere. Ciò significa che il modello è una stringa statica con tag di formattazione al suo interno. Dovrebbe accadere qualcosa alla stringa per dire all'interprete di interpretare la stringa come una nuova stringa f, ma non so se esiste una cosa del genere.

C'è un modo per inserire una stringa e interpretarla come una stringa f per evitare di usare la .format(**locals())chiamata?

Idealmente voglio essere in grado di codificare in questo modo ... (dov'è magic_fstring_functiondove entra la parte che non capisco):

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 questo output desiderato (senza leggere il file due volte):

The current name is foo
The current name is bar

... ma l'output effettivo che ottengo è:

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

5
Non puoi farlo con una fstringa. Una fstringa non è un dato e certamente non è una stringa; è codice. (Controllalo con il dismodulo.) Se vuoi che il codice venga valutato in un secondo momento, usa una funzione.
kindall

12
Cordiali saluti, PEP 501 ha proposto una funzione vicina al tuo primo ideale, ma attualmente è "differita in attesa di ulteriore esperienza con [f-strings]".
jwodder

Un template è una stringa statica, ma una f-string non è una stringa, è un oggetto codice, come ha detto @kindall. Penso che una stringa f sia vincolata alle variabili immediatamente quando viene istanziata (in Python 3.6,7), non quando viene eventualmente utilizzata. Quindi f-string potrebbe essere meno utile del tuo brutto vecchio .format(**locals()), anche se esteticamente più bello. Fino a quando PEP-501 non sarà implementato.
smci

Guido ci salva, ma PEP 498 ha davvero fallito . La valutazione differita descritta da PEP 501 avrebbe dovuto assolutamente essere incorporata nell'implementazione principale di f-string. Ora siamo rimasti a mercanteggiare tra un str.format()metodo meno funzionale ed estremamente lento che supporta la valutazione differita da un lato e una sintassi f-string più funzionale ed estremamente veloce che non supporta la valutazione differita dall'altro. Quindi abbiamo ancora bisogno di entrambi e Python non ha ancora un formattatore di stringhe standard. Inserisci il meme sugli standard xkcd.
Cecil Curry,

Risposte:


26

Ecco un "Ideal 2" completo.

Non è una stringa f — non usa nemmeno le stringhe f — ma fa come richiesto. Sintassi esattamente come specificato. Nessun problema di sicurezza poiché non stiamo utilizzando eval().

Usa una piccola classe e implementa __str__che viene chiamata automaticamente da print. Per sfuggire all'ambito limitato della classe, utilizziamo il inspectmodulo per saltare di un frame e vedere le variabili a cui il chiamante ha accesso.

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

13
La accetterò come risposta, anche se non credo che la userò mai in codice a causa dell'estrema intelligenza. Beh, forse mai :). Forse le persone Python possono usarlo per l'implementazione di PEP 501 . Se la mia domanda fosse "come dovrei gestire questo scenario", la risposta sarebbe "continua a utilizzare la funzione .format () e attendi che PEP 501 si risolva". Grazie per aver capito come fare ciò che non dovrebbe essere fatto, @PaulPanzer
JDAnders

6
Questo non funziona quando il modello include qualcosa di più complesso dei semplici nomi di variabili. Ad esempio: template = "The beginning of the name is {name[:4]}"(-> TypeError: string indices must be integers)
bli

6
@bli Interessante, sembra essere una limitazione di str.format. Pensavo che le stringhe f fossero solo zucchero sintattico per qualcosa di simile, str.format(**locals(), **globals())ma ovviamente mi sbagliavo.
Paul Panzer

4
Si prega di non usarlo in produzione. inspectè una bandiera rossa.
alexandernst

1
Ho 2 domande, perché ispezionare una "bandiera rossa" per la produzione sarebbe un'eccezione in un caso come questo o ci sarebbero soluzioni alternative più praticabili? E c'è qualcosa contro l'uso di __slots__qui per l'utilizzo ridotto della memoria?
Jab

21

Ciò significa che il modello è una stringa statica con tag di formattazione al suo interno

Sì, questo è esattamente il motivo per cui abbiamo letterali con campi sostitutivi e .format, quindi possiamo sostituire i campi ogni volta che vogliamo richiamandoli format.

Qualcosa dovrebbe accadere alla stringa per dire all'interprete di interpretare la stringa come una nuova stringa f

Questo è il prefisso f/F. Potresti racchiuderlo in una funzione e posticipare la valutazione durante il tempo di chiamata, ma ovviamente ciò comporta un sovraccarico aggiuntivo:

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

Che stampa:

The current name is foo
The current name is bar

ma sembra sbagliato ed è limitato dal fatto che puoi solo dare un'occhiata allo spazio dei nomi globale nelle tue sostituzioni. Cercare di usarlo in una situazione che richiede nomi locali fallirà miseramente a meno che non venga passato alla stringa come argomento (che batte totalmente il punto).

C'è un modo per inserire una stringa e interpretarla come una stringa f per evitare di usare la .format(**locals())chiamata?

A parte una funzione (limitazioni incluse), no, quindi potrebbe anche restare .format.


Divertente, avevo pubblicato esattamente lo stesso frammento. Ma l'ho ritirato a causa delle limitazioni dell'ambito. (Prova a racchiudere il ciclo for in una funzione.)
Paul Panzer

@PaulPanzer, forse vuoi modificare la domanda e includerla di nuovo? Non mi dispiacerebbe cancellare la risposta. Questa è una valida alternativa per il caso di OP, non è una valida alternativa per tutti i casi, è subdolo.
Dimitris Fasarakis Hilliard

1
No, va bene, tienilo. Sono molto più contento della mia nuova soluzione. Ma posso vedere il tuo punto che questo è fattibile se sei consapevole dei suoi limiti. Forse potresti aggiungere un piccolo avvertimento al tuo post in modo che nessuno possa sparare ai loro piedi usandolo male?
Paul Panzer

17

Un modo conciso per far valutare una stringa come una stringa f (con tutte le sue capacità) è usare la seguente funzione:

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

Quindi puoi fare:

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

E, a differenza di tante altre soluzioni proposte, puoi anche fare:

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

4
di gran lunga la migliore risposta! come hanno fatto a non includere questa semplice implementazione come caratteristica incorporata quando hanno introdotto le stringhe f?
user3204459

1
no, questo perde la portata. l'unica ragione per cui funziona è perché nameè globale. Le stringhe f dovrebbero essere rimandate nella valutazione, ma la classe FString deve creare un elenco di riferimenti agli argomenti con ambito esaminando le variabili locali e globali del chiamante ... e quindi valutare la stringa quando utilizzata.
Erik Aronesty

2
@ user3204459: Perché essere in grado di eseguire stringhe arbitrarie è intrinsecamente un rischio per la sicurezza, motivo per cui l'uso di eval()è generalmente sconsigliato.
martineau

2
@martineau avrebbe dovuto essere una funzionalità di python in modo che non sia necessario utilizzare eval ... inoltre, f-string ha gli stessi rischi di eval () poiché puoi mettere qualsiasi cosa tra parentesi graffe incluso codice dannoso, quindi se questo è una preoccupazione quindi non utilizzare le stringhe f
user3204459

2
Questo è esattamente quello che stavo cercando, schivando "fstr postpone". Eval non sembra peggiore dell'uso di fstrings in generale, poiché, immagino, possiedono entrambi la stessa potenza: f "{eval ('print (42) ')} "
user2692263

12

Una stringa f è semplicemente un modo più conciso di creare una stringa formattata, sostituendola .format(**names)con f. Se non vuoi che una stringa venga valutata immediatamente in questo modo, non renderla una stringa f. Salvalo come una stringa letterale ordinaria e quindi chiamaformat più tardi quando vuoi eseguire l'interpolazione, come hai fatto.

Ovviamente c'è un'alternativa con eval.

template.txt:

f "Il nome attuale è {name}"

Codice:

>>> 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

Ma poi tutto quello che sei riuscito a fare è sostituire str.formatcon eval, che non è sicuramente la pena. Continua a usare stringhe normali con una formatchiamata.


3
Non vedo davvero alcun vantaggio nel tuo frammento di codice. Voglio dire, puoi sempre scrivere solo The current name is {name}all'interno del template.txtfile e quindi usare print(template_a.format(name=name))(o .format(**locals())). Il codice è più lungo di circa 10 caratteri, ma non introduce eventuali problemi di sicurezza dovuti a eval.
Bakuriu

@Bakuriu - Sì; come ho detto, sebbene evalci consenta di scrivere f'{name}'e ritardare la valutazione namefino a quando non lo si desidera, è inferiore alla semplice creazione di una stringa di modello regolare e quindi a chiamarla format, come già stava facendo l'OP.
TigerhawkT3

4
"Una stringa f è semplicemente un modo più conciso di creare una stringa formattata, sostituendo .format (** nomi) con f." Non proprio: usano una sintassi diversa. Non ho un python3 abbastanza recente con cui controllare, ma ad esempio credo che f '{a + b}' funzioni, mentre '{a + b}'. Format (a = a, b = b) solleva KeyError . .format () probabilmente va bene in molti contesti, ma non è una sostituzione immediata.
philh

2
@philh Penso che ho appena incontrato un esempio in cui .formatnon è equivalente ad un F-string, in grado di supportare si commento: DNA = "TATTCGCGGAAAATATTTTGA"; fragment = f"{DNA[2:8]}"; failed_fragment = "{DNA[2:8]}".format(**locals()). Il tentativo di creare failed_fragmentrisultati in TypeError: string indices must be integers.
bli

12

L'utilizzo di .format non è una risposta corretta a questa domanda. Le f-string di Python sono molto diverse dai modelli str.format () ... possono contenere codice o altre operazioni costose - da qui la necessità di differimento.

Ecco un esempio di logger differito. Questo utilizza il normale preambolo di logging.getLogger, ma poi aggiunge nuove funzioni che interpretano la stringa f solo se il livello di log è corretto.

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)

Questo ha il vantaggio di poter fare cose come: log.fdebug("{obj.dump()}").... senza scaricare l'oggetto a meno che il debug non sia abilitato.

IMHO: Questa avrebbe dovuto essere l' operazione predefinita di f-strings, tuttavia ora è troppo tardi . La valutazione della stringa F può avere effetti collaterali enormi e non intenzionali e il fatto che ciò avvenga in modo differito cambierà l'esecuzione del programma.

Per rendere le stringhe f correttamente differite, python avrebbe bisogno di un modo per cambiare esplicitamente il comportamento. Forse usare la lettera "g"? ;)

È stato sottolineato che la registrazione differita non dovrebbe bloccarsi se c'è un bug nel convertitore di stringhe. La soluzione di cui sopra può fare anche questo, cambiare il finally:a except:e inserire un log.exceptionlì.


1
D'accordo con questa risposta di tutto cuore. Questo caso d'uso è ciò a cui stavo pensando durante la ricerca di questa domanda.
metà

1
Questa è la risposta corretta. Alcuni orari: %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

Quello che vuoi sembra essere considerato come un miglioramento di Python .

Nel frattempo, dalla discussione collegata, quanto segue sembra essere una soluzione alternativa ragionevole che non richiede l'utilizzo 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)

Produzione:

The current name, number is 'foo', 41
The current name, number is 'bar', 42

7

ispirato dalla risposta di kadee , quanto segue può essere utilizzato per definire una classe deferred-f-string.

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)

che è esattamente ciò che chiedeva la domanda


4

O forse non utilizzare stringhe f, basta formattare:

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

Nella versione senza nomi:

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

Questo non funziona in tutti i casi. Esempio: fun = "{DNA[2:8]}".format; DNA = "TATTCGCGGAAAATATTTTGA"; fun(DNA=DNA). ->TypeError: string indices must be integers
bli

Ma non funziona anche durante il normale uso, si prega di guardare risposta stackoverflow.com/questions/14072810/...
msztolcman

2

Che ne dite di:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

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

0

Un suggerimento che utilizza stringhe f. Esegui la tua valutazione sul livello logico in cui si sta verificando il modello e passalo come generatore. Puoi rilassarlo in qualsiasi punto tu scelga, usando le stringhe f

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  
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.