Jak odłożyć / odroczyć ocenę f-stringów?

105

Używam ciągów szablonów do generowania niektórych plików i uwielbiam zwięzłość nowych ciągów f do tego celu, aby zredukować mój poprzedni kod szablonu z czegoś takiego:

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

Teraz mogę to zrobić, bezpośrednio zastępując zmienne:

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

Czasami jednak sensowne jest zdefiniowanie szablonu w innym miejscu - wyżej w kodzie lub zaimportowanie z pliku lub czegoś podobnego. Oznacza to, że szablon jest statycznym ciągiem zawierającym znaczniki formatujące. Coś musiałoby się stać ze stringiem, aby powiedzieć interpreterowi, aby zinterpretował go jako nową f-string, ale nie wiem, czy coś takiego istnieje.

Czy istnieje sposób, aby wprowadzić ciąg i interpretować go jako ciąg f, aby uniknąć używania .format(**locals())wywołania?

Idealnie chciałbym móc kodować w ten sposób ... (gdzie magic_fstring_functionjest część, której nie rozumiem):

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)

... z tym żądanym wyjściem (bez dwukrotnego odczytu pliku):

The current name is foo
The current name is bar

... ale rzeczywisty wynik, jaki otrzymuję, to:

The current name is {name}
The current name is {name}
JDAnders
źródło
5
Nie możesz tego zrobić za pomocą fsznurka. fCiąg nie jest dane, a to z pewnością nie jest to ciąg znaków; to jest kod. (Sprawdź to z dismodułem). Jeśli chcesz, aby kod był oceniany w późniejszym czasie, użyj funkcji.
kindall
12
FYI, PEP 501 zaproponował funkcję bliską Twojemu pierwszemu ideałowi, ale obecnie jest ona „odroczona do czasu dalszych doświadczeń z [f-strings]”.
jwodder
Szablon jest ciągiem statycznym, ale f-string nie jest łańcuchem, to obiekt kodu, jak powiedział @kindall. Myślę, że f-string jest powiązany ze zmiennymi natychmiast po utworzeniu (w Pythonie 3.6,7), a nie wtedy, gdy zostanie ostatecznie użyty. Więc stringi mogą być mniej przydatne niż twój brzydki stary .format(**locals()), chociaż kosmetycznie ładniejszy. Do czasu wdrożenia PEP-501.
smci
Guido nas ocal, ale PEP 498 naprawdę to spartaczył . Odroczona ocena opisana przez PEP 501 absolutnie powinna zostać wprowadzona do podstawowej implementacji f-string. Teraz musimy się targować między mniej funkcjonalną, ekstremalnie powolną str.format()metodą obsługującą odroczoną ocenę z jednej strony, a bardziej funkcjonalną, niezwykle szybką składnią f-string, która nie obsługuje odroczonej oceny z drugiej. Więc nadal potrzebujemy obu, a Python nadal nie ma standardowego programu formatującego ciągi. Wstaw meme standardów xkcd.
Cecil Curry,

Odpowiedzi:

26

Oto kompletny „Idealny 2”.

To nie jest ciąg f - nie używa nawet ciągów f - ale działa zgodnie z żądaniem. Składnia dokładnie taka, jak określono. Żadnych problemów związanych z bezpieczeństwem, ponieważ nie używamy eval().

Używa małej klasy i narzędzi, __str__które są automatycznie wywoływane przez print. Aby uciec od ograniczonego zakresu klasy, używamy inspectmodułu, aby przeskoczyć jedną ramkę w górę i zobaczyć zmienne, do których ma dostęp wywołujący.

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
źródło
14
Przyjmę to jako odpowiedź, chociaż nie sądzę, żebym kiedykolwiek użył tego w kodzie z powodu niezwykłej sprytu. No może nigdy :). Może ludzie z Pythona mogą go użyć do implementacji PEP 501 . Jeśli moje pytanie brzmiało „jak mam sobie poradzić z tym scenariuszem”, odpowiedź brzmiałaby „po prostu używaj funkcji .format () i poczekaj, aż PEP 501 się rozwiąże”. Dzięki za zastanowienie się, jak zrobić to, czego nie należy robić, @PaulPanzer
JDAnders
7
To nie działa, gdy szablon zawiera coś bardziej złożonego niż proste nazwy zmiennych. Na przykład: template = "The beginning of the name is {name[:4]}"(-> TypeError: string indices must be integers)
bli
6
@bli Ciekawe, wydaje się być ograniczeniem str.format. Kiedyś myślałem, że f-stringi są po prostu cukrem syntaktycznym na coś takiego, str.format(**locals(), **globals())ale oczywiście się myliłem.
Paul Panzer,
4
Nie używaj tego w produkcji. inspectto czerwona flaga.
alexandernst
1
Mam 2 pytania, dlaczego sprawdzam „czerwoną flagę” pod kątem produkcji, czy przypadek taki jak ten byłby wyjątkiem, czy też byłyby bardziej realne obejścia? Czy jest coś przeciwko używaniu __slots__tutaj do zmniejszonego zużycia pamięci?
Jab
22

Oznacza to, że szablon jest statycznym ciągiem zawierającym znaczniki formatujące

Tak, właśnie dlatego mamy literały z polami zastępczymi .format, dzięki czemu możemy zastępować pola w dowolnym momencie, wywołując formatje.

Coś musiałoby się stać z łańcuchem, aby powiedzieć interpreterowi, aby zinterpretował łańcuch jako nowy f-string

To jest przedrostek f/F. Możesz owinąć to w funkcję i odłożyć ocenę w czasie połączenia, ale oczywiście wiąże się to z dodatkowym narzutem:

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

Który drukuje:

The current name is foo
The current name is bar

ale czuje się źle i jest ograniczony przez fakt, że możesz tylko zerknąć na globalną przestrzeń nazw w swoich zamiennikach. Próba użycia go w sytuacji, która wymaga nazw lokalnych, zakończy się niepowodzeniem, chyba że zostanie przekazana do łańcucha jako argumenty (co całkowicie mija się z celem).

Czy istnieje sposób, aby wprowadzić ciąg i interpretować go jako ciąg f, aby uniknąć używania .format(**locals())wywołania?

Inna niż funkcja (w tym ograniczenia), nie, więc równie dobrze może się trzymać .format.

Dimitris Fasarakis Hilliard
źródło
Zabawne, opublikowałem dokładnie ten sam fragment. Ale wycofałem to z powodu ograniczeń zakresu. (Spróbuj zawinąć pętlę for w funkcję.)
Paul Panzer,
@PaulPanzer może chcesz edytować pytanie i ponownie je dołączyć? Nie miałbym nic przeciwko usunięciu odpowiedzi. Jest to realna alternatywa dla przypadku OP, nie jest to realna alternatywa dla wszystkich przypadków, jest podstępna.
Dimitris Fasarakis Hilliard
1
Nie, w porządku, zatrzymaj to. Jestem znacznie bardziej zadowolony z mojego nowego rozwiązania. Ale widzę twój punkt widzenia, że ​​ten jest opłacalny, jeśli zdajesz sobie sprawę z jego ograniczeń. Może mógłbyś dodać trochę ostrzeżenia do swojego postu, aby nikt nie mógł strzelić sobie stopy, używając go źle?
Paul Panzer
18

Zwięzłym sposobem na ocenę łańcucha jako f-string (z jego pełnymi możliwościami) jest użycie następującej funkcji:

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

Następnie możesz:

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

W przeciwieństwie do wielu innych proponowanych rozwiązań możesz również:

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
źródło
4
zdecydowanie najlepsza odpowiedź! w jaki sposób nie uwzględnili tej prostej implementacji jako wbudowanej funkcji, kiedy wprowadzili f-stringi?
user3204459
1
nie, to traci zasięg. jedynym powodem, dla którego działa, jest to, że namejest globalny. Ciągi f powinny zostać odroczone w ocenie, ale klasa FString musi utworzyć listę odwołań do argumentów w określonym zakresie, patrząc na lokalne i globalne wywołań, a następnie oceniać łańcuch, gdy jest używany.
Erik Aronesty,
2
@ user3204459: Ponieważ możliwość wykonania dowolnych ciągów znaków jest z natury zagrożeniem bezpieczeństwa - dlatego eval()generalnie odradza się używanie .
martineau
2
@martineau powinno to być cechą Pythona, więc nie musisz używać eval ... plus, f-string ma takie samo ryzyko jak eval (), ponieważ możesz umieścić wszystko w nawiasach klamrowych, w tym złośliwy kod, więc jeśli to jest niepokój to nie używaj f-stringów
user3204459
2
To jest dokładnie to, czego szukałem, unikając „fstr odroczenia”. Eval nie wydaje się gorszy niż ogólne użycie fstrings, ponieważ, jak sądzę, oba mają tę samą moc: f "{eval ('print (42) ')} "
user2692263
12

F-string to po prostu bardziej zwięzły sposób tworzenia sformatowanego ciągu, zastępując .format(**names)go f. Jeśli nie chcesz, aby ciąg był natychmiast oceniany w taki sposób, nie rób z niego f-string. Zapisz go jako zwykły literał łańcuchowy, a następnie przywołaj formatgo później, gdy chcesz wykonać interpolację, tak jak to robiłeś.

Oczywiście istnieje alternatywa z eval.

template.txt:

f „Obecna nazwa to {name}”

Kod:

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

Ale to wszystko, co udało się zrobić, to wymienić str.formatsię eval, co nie jest z pewnością warto. Po prostu używaj zwykłych ciągów znaków podczas formatwywołania.

TigerhawkT3
źródło
3
Naprawdę nie widzę żadnej korzyści w twoim fragmencie kodu. Chodzi mi o to, że zawsze możesz pisać tylko The current name is {name}w template.txtpliku, a następnie użyć print(template_a.format(name=name))(lub .format(**locals())). Kod jest dłuższy o około 10 znaków, ale nie wprowadza żadnych możliwych problemów związanych z bezpieczeństwem eval.
Bakuriu
@Bakuriu - Tak; jak powiedziałem, chociaż evalpozwala nam napisać f'{name}'i opóźnić ocenę namedo pożądanego, jest gorszy od zwykłego utworzenia zwykłego ciągu szablonu, a następnie wywołania formatgo, tak jak robił to OP.
TigerhawkT3
4
„F-string jest po prostu bardziej zwięzłym sposobem tworzenia sformatowanego łańcucha, zastępując .format (** nazwy) przez f”. Niezupełnie - używają innej składni. Nie mam wystarczająco świeżego Pythona3 do sprawdzenia, ale na przykład uważam, że f '{a + b}' działa, podczas gdy '{a + b}'. Format (a = a, b = b) wywołuje KeyError . Format .format () jest prawdopodobnie dobry w wielu kontekstach, ale nie jest zastępstwem typu drop-in.
philh
2
@philh Chyba właśnie napotkał przykład, w którym .formatnie jest równoznaczne z f-string, które mogą Ci pomóc skomentować: DNA = "TATTCGCGGAAAATATTTTGA"; fragment = f"{DNA[2:8]}"; failed_fragment = "{DNA[2:8]}".format(**locals()). Próba stworzenia failed_fragmentwyników w TypeError: string indices must be integers.
bli
12

Używanie .format nie jest poprawną odpowiedzią na to pytanie. Pythonowe f-stringi bardzo różnią się od szablonów str.format () ... mogą zawierać kod lub inne kosztowne operacje - stąd potrzeba odroczenia.

Oto przykład odroczonego rejestratora. Używa normalnej preambuły logowania .getLogger, ale dodaje nowe funkcje, które interpretują f-string tylko wtedy, gdy poziom dziennika jest poprawny.

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)

Ma to tę zaletę, że można robić takie rzeczy jak: log.fdebug("{obj.dump()}").... bez zrzucania obiektu, chyba że debugowanie jest włączone.

IMHO: To powinno być domyślne działanie f-stringów, ale teraz jest już za późno . Ocena łańcucha F może mieć ogromne i niezamierzone skutki uboczne, a wystąpienie tego w sposób odroczony zmieni wykonywanie programu.

Aby poprawnie odroczyć ciągi f, Python potrzebowałby jakiegoś sposobu jawnego przełączania zachowania. Może użyj litery „g”? ;)

Wskazano, że odroczone rejestrowanie nie powinno ulegać awarii, jeśli w konwerterze ciągów wystąpi błąd. Powyższe rozwiązanie można to zrobić również, zmienić finally:się except:i trzymać log.exceptiontam.

Erik Aronesty
źródło
1
Zgadzam się całym sercem z tą odpowiedzią. Ten przypadek użycia jest tym, o czym myślałem, szukając tego pytania.
justhalf
1
To jest poprawna odpowiedź. Niektóre terminy: %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

Wydaje się, że to, czego chcesz, jest uważane za ulepszenie Pythona .

Tymczasem - z połączonej dyskusji - wydaje się, że byłoby to rozsądne obejście, które nie wymaga użycia 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)

Wynik:

The current name, number is 'foo', 41
The current name, number is 'bar', 42
martineau
źródło
7

zainspirowany odpowiedzią Kadee , do zdefiniowania klasy odroczonego f-string można użyć następującego.

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)

co jest dokładnie tym, o co chodziło w pytaniu

user3204459
źródło
4

A może nie używaj f-stringów, po prostu formatuj:

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

W wersji bez nazw:

fun = "The curent name is {}".format
names = ["foo", "bar"]
for name in names:
    print(fun(name))
msztolcman
źródło
To nie działa we wszystkich przypadkach. Przykład: fun = "{DNA[2:8]}".format; DNA = "TATTCGCGGAAAATATTTTGA"; fun(DNA=DNA). ->TypeError: string indices must be integers
bli
Ale to nie działa również przy normalnym użytkowaniu, spójrz na odpowiedź stackoverflow.com/questions/14072810/ ...
msztolcman
2

Co powiesz na:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

s.format(foo='Bar')
> 'Hi, Bar!'
Denis
źródło
0

Sugestia wykorzystująca f-stringi. Dokonaj oceny na poziomie logicznym, na którym ma miejsce tworzenie szablonów i przekaż ją jako generator. Możesz go rozwinąć w dowolnym momencie, używając f-stringów

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
źródło