Wie kann die Bewertung von F-Strings verschoben / verschoben werden?


94

Ich verwende Vorlagenzeichenfolgen, um einige Dateien zu generieren, und ich liebe die Prägnanz der neuen F-Zeichenfolgen für diesen Zweck, um meinen vorherigen Vorlagencode von so etwas zu reduzieren:

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

Jetzt kann ich dies tun und Variablen direkt ersetzen:

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

Manchmal ist es jedoch sinnvoll, die Vorlage an einer anderen Stelle zu definieren - weiter oben im Code oder aus einer Datei oder etwas anderem importiert. Dies bedeutet, dass die Vorlage eine statische Zeichenfolge mit Formatierungs-Tags ist. Es müsste etwas mit der Zeichenfolge passieren, um den Interpreter anzuweisen, die Zeichenfolge als neue F-Zeichenfolge zu interpretieren, aber ich weiß nicht, ob es so etwas gibt.

Gibt es eine Möglichkeit, eine Zeichenfolge einzubringen und als F-Zeichenfolge zu interpretieren, um die Verwendung des .format(**locals())Aufrufs zu vermeiden ?

Idealerweise möchte ich in der Lage sein, so zu codieren ... (wo magic_fstring_functionkommt der Teil ins Spiel, den ich nicht verstehe):

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)

... mit dieser gewünschten Ausgabe (ohne die Datei zweimal zu lesen):

The current name is foo
The current name is bar

... aber die tatsächliche Ausgabe, die ich bekomme, ist:

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

5
Mit einem fString kann man das nicht machen . Eine fZeichenfolge besteht nicht aus Daten, und es handelt sich sicherlich nicht um eine Zeichenfolge. Es ist Code. (Überprüfen Sie dies mit dem disModul.) Wenn Sie möchten, dass Code zu einem späteren Zeitpunkt ausgewertet wird, verwenden Sie eine Funktion.
Kindall

10
Zu Ihrer Information, PEP 501 hat eine Funktion vorgeschlagen, die Ihrem ersten Ideal nahe kommt, die jedoch derzeit "verschoben wird, bis weitere Erfahrungen mit [F-Strings] vorliegen".
Jwodder

Eine Vorlage ist eine statische Zeichenfolge, aber eine F-Zeichenfolge ist keine Zeichenfolge, sondern ein Codeobjekt, wie @kindall sagte. Ich denke, ein F-String wird sofort an Variablen gebunden, wenn er instanziiert wird (in Python 3.6,7), nicht wenn er schließlich verwendet wird. F-String kann also weniger nützlich sein als Ihr hässlicher alter .format(**locals()), obwohl kosmetisch schöner. Bis PEP-501 implementiert ist.
smci

Antworten:


25

Hier ist ein komplettes "Ideal 2".

Es ist kein F-String - es werden nicht einmal F-Strings verwendet - aber es funktioniert wie gewünscht. Syntax genau wie angegeben. Keine Sicherheitskopfschmerzen, da wir nicht verwenden eval().

Es verwendet eine kleine Klasse und implementiert, __str__die automatisch von print aufgerufen wird. Um dem begrenzten Umfang der Klasse zu entgehen, verwenden wir das inspectModul, um einen Frame nach oben zu springen und die Variablen anzuzeigen, auf die der Aufrufer Zugriff hat.

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

10
Ich werde dies als Antwort akzeptieren, obwohl ich nicht glaube, dass ich es wegen der extremen Klugheit jemals im Code verwenden werde. Na vielleicht nie :). Vielleicht können die Python-Leute es für die Implementierung von PEP 501 verwenden . Wenn meine Fragen lauteten: "Wie soll ich mit diesem Szenario umgehen?", Lautete die Antwort "Verwenden Sie einfach weiterhin die Funktion .format () und warten Sie, bis PEP 501 behoben ist." Vielen Dank, dass Sie herausgefunden haben, wie Sie das tun sollen, was nicht getan werden sollte. @PaulPanzer
JDAnders

6
Dies funktioniert nicht, wenn die Vorlage etwas Komplexeres als einfache Variablennamen enthält. Zum Beispiel: template = "The beginning of the name is {name[:4]}"(-> TypeError: string indices must be integers)
bli

6
@bli Interessant, scheint eine Einschränkung von zu sein str.format. Früher dachte ich, F-Strings seien nur syntaktischer Zucker für so etwas, str.format(**locals(), **globals())aber offensichtlich habe ich mich geirrt.
Paul Panzer

3
Bitte verwenden Sie das nicht in der Produktion. inspectist eine rote Fahne.
Alexander

1
Ich habe zwei Fragen: Warum wird eine "rote Fahne" für die Produktion überprüft? Wäre ein Fall wie dieser eine Ausnahme oder gäbe es praktikablere Problemumgehungen? Und gibt es etwas gegen die Verwendung von __slots__hier für die reduzierte Speichernutzung?
Jab

19

Dies bedeutet, dass die Vorlage eine statische Zeichenfolge mit Formatierungs-Tags ist

Ja, genau deshalb haben wir Literale mit Ersatzfeldern und .formatkönnen die Felder jederzeit ersetzen, indem wir sie aufrufen format.

Mit dem String müsste etwas passieren, um den Interpreter anzuweisen, den String als neuen F-String zu interpretieren

Das ist das Präfix f/F. Sie könnten es in eine Funktion einbinden und die Auswertung während der Anrufzeit verschieben, aber das verursacht natürlich zusätzlichen Aufwand:

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

Welches druckt aus:

The current name is foo
The current name is bar

fühlt sich aber falsch an und wird durch die Tatsache eingeschränkt, dass Sie nur einen Blick auf den globalen Namespace in Ihren Ersetzungen werfen können. Der Versuch, es in einer Situation zu verwenden, in der lokale Namen erforderlich sind, schlägt kläglich fehl, es sei denn, es wird als Argument an die Zeichenfolge übergeben (was den Punkt völlig übertrifft).

Gibt es eine Möglichkeit, eine Zeichenfolge einzubringen und als F-Zeichenfolge zu interpretieren, um die Verwendung des .format(**locals())Aufrufs zu vermeiden ?

Anders als eine Funktion (einschließlich Einschränkungen), nein, könnte genauso gut bei bleiben .format.


Komisch, ich hatte genau das gleiche Snippet gepostet. Aber ich habe es wegen Einschränkungen des Anwendungsbereichs zurückgezogen. (Versuchen Sie, die for-Schleife in eine Funktion
Paul Panzer

@PaulPanzer Möchten Sie die Frage vielleicht bearbeiten und wieder aufnehmen? Es würde mir nichts ausmachen, die Antwort zu löschen. Dies ist eine praktikable Alternative für den Fall von OP. Es ist nicht für alle Fälle eine praktikable Alternative , es ist hinterhältig.
Dimitris Fasarakis Hilliard

1
Nein, es ist in Ordnung, behalte es. Ich bin viel zufriedener mit meiner neuen Lösung. Aber ich kann Ihren Standpunkt sehen, dass dieser realisierbar ist, wenn Sie sich seiner Grenzen bewusst sind. Vielleicht könnten Sie Ihrem Beitrag eine kleine Warnung hinzufügen, damit niemand seinen Fuß schießen kann, wenn er ihn falsch verwendet?
Paul Panzer

13

Eine prägnante Möglichkeit, eine Zeichenfolge als F-Zeichenfolge (mit ihren vollen Funktionen) auszuwerten, besteht in der Verwendung der folgenden Funktion:

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

Dann können Sie tun:

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

Und im Gegensatz zu vielen anderen Lösungsvorschlägen können Sie auch Folgendes tun:

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

3
bei weitem die beste Antwort! Wie haben sie diese einfache Implementierung nicht als integrierte Funktion aufgenommen, als sie F-Strings eingeführt haben?
user3204459

1
Nein, das verliert an Umfang. Der einzige Grund, der funktioniert, nameist global. f-Strings sollten bei der Auswertung zurückgestellt werden, aber die Klasse FString muss eine Liste von Verweisen auf die Argumente mit Gültigkeitsbereich erstellen, indem sie die lokalen und globalen Aufrufer des Aufrufers betrachtet ... und dann den String auswertet, wenn er verwendet wird.
Erik Aronesty

@ user3204459: Da die Ausführung beliebiger Zeichenfolgen von Natur aus ein Sicherheitsrisiko darstellt, eval()wird generell von der Verwendung von abgeraten.
Martineau

1
@martineau Es sollte eine Funktion von Python sein, damit Sie eval nicht verwenden müssen. Außerdem birgt f-string die gleichen Risiken wie eval (), da Sie alles in geschweifte Klammern setzen können, einschließlich bösartigen Codes Ein
Problem,

1
Dies ist genau das, wonach ich gesucht habe, um mich nach 'fstr postpone' zu ducken. Eval scheint nicht schlechter zu sein als die Verwendung von fstrings im Allgemeinen, da beide vermutlich die gleiche Kraft besitzen: f "{eval ('print (42)) ')} "
user2692263

12

Eine F-Zeichenfolge ist einfach eine präzisere Methode zum Erstellen einer formatierten Zeichenfolge, die .format(**names)durch ersetzt wird f. Wenn Sie nicht möchten, dass eine Zeichenfolge sofort auf diese Weise ausgewertet wird, machen Sie sie nicht zu einer F-Zeichenfolge. Speichern Sie es als gewöhnliches Zeichenfolgenliteral und rufen Sie formates später auf, wenn Sie die Interpolation wie bisher ausführen möchten.

Natürlich gibt es eine Alternative mit eval.

template.txt::

f'Der aktuelle Name ist {name} '

Code:

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

Aber dann haben Sie es geschafft , zu tun ist , zu ersetzen str.formatmit eval, das ist sicherlich nicht wert. Verwenden Sie bei einem formatAufruf einfach weiterhin normale Zeichenfolgen .


3
Ich sehe wirklich keinen Vorteil in Ihrem Codeausschnitt. Ich meine, Sie können immer nur The current name is {name}in die template.txtDatei schreiben und dann print(template_a.format(name=name))(oder .format(**locals())) verwenden. Der Code ist ungefähr 10 Zeichen länger, führt jedoch aufgrund von Sicherheitsproblemen nicht zu möglichen Problemen eval.
Bakuriu

@ Bakuriu - Ja; Wie ich bereits sagte, obwohl evales uns erlaubt, f'{name}'die Auswertung namebis zum gewünschten Zeitpunkt zu schreiben und zu verzögern , ist es minderwertig, einfach eine reguläre Vorlagenzeichenfolge zu erstellen und diese dann aufzurufen format, wie es das OP bereits getan hat.
TigerhawkT3

3
"Eine f-Zeichenfolge ist einfach eine präzisere Methode zum Erstellen einer formatierten Zeichenfolge, bei der .format (** Namen) durch f ersetzt wird." Nicht ganz - sie verwenden unterschiedliche Syntax. Ich habe nicht genügend Python3, mit dem ich nachsehen kann, aber ich glaube zum Beispiel, dass f '{a + b}' funktioniert, während das Format '{a + b}' (a = a, b = b) KeyError auslöst . .format () ist wahrscheinlich in vielen Zusammenhängen in Ordnung, aber kein Drop-In-Ersatz.
Philh

1
@philh Ich glaube, ich bin gerade auf ein Beispiel gestoßen, bei dem .formates sich nicht um einen F-String handelt, der Sie bei folgenden Kommentaren unterstützen kann : DNA = "TATTCGCGGAAAATATTTTGA"; fragment = f"{DNA[2:8]}"; failed_fragment = "{DNA[2:8]}".format(**locals()). Der Versuch, failed_fragmentErgebnisse zu erstellen, führt zu TypeError: string indices must be integers.
Bli

9

Die Verwendung von .format ist keine korrekte Antwort auf diese Frage. Python-F-Strings unterscheiden sich stark von str.format () -Vorlagen. Sie können Code oder andere teure Operationen enthalten - daher ist eine Verschiebung erforderlich.

Hier ist ein Beispiel für einen verzögerten Logger. Dies verwendet die normale Präambel von logging.getLogger, fügt dann aber neue Funktionen hinzu, die den f-String nur interpretieren, wenn die Protokollstufe korrekt ist.

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)

Dies hat den Vorteil, dass Sie Folgendes tun können: log.fdebug("{obj.dump()}").... ohne das Objekt zu sichern, es sei denn, das Debuggen ist aktiviert.

IMHO: Dies hätte die Standardoperation von F-Strings sein sollen, aber jetzt ist es zu spät . Die Auswertung von F-Strings kann massive und unbeabsichtigte Nebenwirkungen haben, und wenn dies auf verzögerte Weise geschieht, ändert sich die Programmausführung.

Um F-Strings richtig zu verschieben, würde Python eine Möglichkeit benötigen, das Verhalten explizit zu ändern. Vielleicht den Buchstaben 'g' verwenden? ;)


Stimmen Sie dieser Antwort von ganzem Herzen zu. An diesen Anwendungsfall habe ich bei der Suche nach dieser Frage gedacht.
halb

Dies ist die richtige Antwort. Einige Zeiten: %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

Was Sie wollen, scheint als Python- Erweiterung betrachtet zu werden .

In der verknüpften Diskussion scheint Folgendes eine vernünftige Problemumgehung zu sein, für die keine Verwendung erforderlich ist 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)

Ausgabe:

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

7

Inspiriert von der Antwort von Kadee kann das Folgende verwendet werden, um eine verzögerte F-String-Klasse zu definieren.

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)

Das ist genau das, wonach die Frage gestellt wurde


4

Oder verwenden Sie keine F-Strings, sondern formatieren Sie einfach:

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

In Version ohne Namen:

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

Dies funktioniert nicht in allen Fällen. Beispiel : fun = "{DNA[2:8]}".format; DNA = "TATTCGCGGAAAATATTTTGA"; fun(DNA=DNA). ->TypeError: string indices must be integers
Bli

Aber es funktioniert auch nicht im normalen Gebrauch, siehe Antwort stackoverflow.com/questions/14072810/…
msztolcman

0

Ein Vorschlag, der F-Strings verwendet. Führen Sie Ihre Bewertung auf der logischen Ebene durch, auf der das Templating stattfindet, und übergeben Sie es als Generator. Sie können es an jedem beliebigen Punkt mit F-Strings abwickeln

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  

0

Wie wäre es mit:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

s.format(foo='Bar')
> 'Hi, Bar!'
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.