Как отложить / отложить оценку f-строк?

102

Я использую строки шаблона для генерации некоторых файлов, и мне нравится лаконичность новых f-строк для этой цели, за то, что мой предыдущий код шаблона был сокращен примерно так:

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

Теперь я могу это сделать, напрямую заменив переменные:

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

Однако иногда имеет смысл определить шаблон в другом месте - выше в коде, или импортированный из файла или чего-то еще. Это означает, что шаблон представляет собой статическую строку с тегами форматирования в ней. Что-то должно произойти со строкой, чтобы интерпретатор интерпретировал строку как новую f-строку, но я не знаю, существует ли такая вещь.

Есть ли способ ввести строку и интерпретировать ее как f-строку, чтобы избежать использования .format(**locals())вызова?

В идеале я хочу иметь возможность кодировать вот так ... (где magic_fstring_functionнаходится часть, которую я не понимаю):

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)

... с этим желаемым результатом (без двойного чтения файла):

The current name is foo
The current name is bar

... но фактический результат, который я получаю:

The current name is {name}
The current name is {name}
JDAnders
источник
5
Вы не можете сделать это со fстрокой. fСтрока не данные, и это, безусловно , не является строка; это код. (Проверьте это с помощью disмодуля.) Если вы хотите, чтобы код был оценен позже, вы используете функцию.
kindall
12
К вашему сведению, PEP 501 предлагает функцию, близкую к вашему первому идеалу, но в настоящее время она «отложена до дальнейшего опыта с [f-strings]».
jwodder
Шаблон - это статическая строка, но f-строка - это не строка, это объект кода, как сказал @kindall. Я думаю, что f-строка связывается с переменными сразу после ее создания (в Python 3.6,7), а не когда она в конечном итоге используется. Так что f-струна может быть менее полезной, чем ваша уродливая старая .format(**locals()), хотя косметически лучше. Пока не будет реализован PEP-501.
smci
Гвидо, спаси нас, но PEP 498 действительно все испортил . Отложенная оценка, описанная в PEP 501, абсолютно должна была быть встроена в основную реализацию f-string. Теперь нам остается торговать между менее функциональным и чрезвычайно медленным str.format()методом, поддерживающим отложенное вычисление, с одной стороны, и более функциональным, чрезвычайно быстрым синтаксисом f-строки, не поддерживающим отложенное вычисление, с другой. Так что нам все еще нужны оба, а в Python по-прежнему нет стандартного средства форматирования строк. Вставьте мем стандартов xkcd.
Сесил Карри,

Ответы:

26

Вот и полный «Идеал 2».

Это не f-строка - она ​​даже не использует f-строки - но выполняет то, что было запрошено. Синтаксис точно такой, как указано. Никаких проблем с безопасностью, поскольку мы не используем eval().

Он использует небольшой класс и реализует, __str__который автоматически вызывается print. Чтобы избежать ограниченной области действия класса, мы используем inspectмодуль для перехода на один кадр вверх и просмотра переменных, к которым у вызывающего абонента есть доступ.

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
Я собираюсь принять это как ответ, хотя я не думаю, что когда-либо буду использовать его в коде из-за чрезвычайного ума. Ну может никогда :). Возможно, питонисты могут использовать его для реализации PEP 501 . Если бы мои вопросы были «как мне справиться с этим сценарием», ответ был бы «просто продолжайте использовать функцию .format () и дождитесь разрешения PEP 501». Спасибо, что выяснили, как делать то, чего делать не следует, @PaulPanzer
JDAnders
6
Это не работает, если шаблон включает что-то более сложное, чем простые имена переменных. Например: template = "The beginning of the name is {name[:4]}"(-> TypeError: string indices must be integers)
bli
6
@bli Интересно, вроде ограничение str.format. Раньше я думал, что f-строки - это просто синтаксический сахар для чего-то вроде, str.format(**locals(), **globals())но, очевидно, я был неправ.
Paul Panzer
4
Пожалуйста, не используйте это в производстве. inspectэто красный флаг.
alexandernst
1
У меня 2 вопроса: почему проверка «красного флага» для производства может быть исключением, или есть более жизнеспособные обходные пути? И есть ли что-то против использования __slots__здесь для уменьшения использования памяти?
Jab
21

Это означает, что шаблон представляет собой статическую строку с тегами форматирования в ней.

Да, именно поэтому у нас есть литералы с заменяющими полями, и .formatпоэтому мы можем заменить поля, когда захотим, вызвав formatего.

Что-то должно произойти со строкой, чтобы интерпретатор интерпретировал строку как новую f-строку

Это приставка f/F. Вы можете обернуть его функцией и отложить оценку во время вызова, но, конечно, это повлечет за собой дополнительные накладные расходы:

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

Что распечатывает:

The current name is foo
The current name is bar

но кажется неправильным и ограничивается тем фактом, что вы можете только взглянуть на глобальное пространство имен в ваших заменах. Попытка использовать его в ситуации, требующей локальных имен, потерпит неудачу, если не будет передана в строку в качестве аргументов (что полностью превосходит суть).

Есть ли способ ввести строку и интерпретировать ее как f-строку, чтобы избежать использования .format(**locals())вызова?

Помимо функции (включая ограничения), нет, так что можно придерживаться .format.

Димитрис Фасаракис Хиллиард
источник
Забавно, я опубликовал точно такой же фрагмент. Но я отозвал его из-за ограничений объема работ. (Попробуйте обернуть цикл for в функцию.)
Пол Панзер
@PaulPanzer, может быть, вы хотите отредактировать вопрос и снова включить его? Я бы не прочь удалить ответ. Это жизнеспособная альтернатива для случая OP, это не жизнеспособная альтернатива для всех случаев, это хитрость.
Димитрис Фасаракис Хиллиард
1
Нет, все нормально, оставь себе. Я гораздо больше доволен своим новым решением. Но я могу понять вашу точку зрения, что этот вариант жизнеспособен, если вы знаете о его ограничениях. Может быть, вы могли бы добавить небольшое предупреждение в свой пост, чтобы никто не мог прострелить себе ногу, используя его неправильно?
Paul Panzer
17

Краткий способ оценки строки как f-строки (с ее полными возможностями) заключается в использовании следующей функции:

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

Тогда вы сможете:

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

И, в отличие от многих других предлагаемых решений, вы также можете:

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
безусловно лучший ответ! как они не включили эту простую реализацию в качестве встроенной функции, когда представили f-строки?
user3204459
1
нет, это теряет размах. единственная причина, по которой это работает, - nameэто глобальность. f-строки должны быть отложены при оценке, но классу FString необходимо создать список ссылок на аргументы с заданной областью действия, просмотрев локальные и глобальные переменные вызывающих объектов ... и затем оценить строку при использовании.
Эрик Аронести
2
@ user3204459: Потому что возможность выполнять произвольные строки по своей сути представляет угрозу безопасности - вот почему использование eval()обычно не рекомендуется.
martineau
2
@martineau это должна была быть особенность python, поэтому вам не нужно использовать eval ... плюс, f-строка имеет те же риски, что и eval (), поскольку вы можете заключить в фигурные скобки все, что угодно, включая вредоносный код, поэтому, если это проблема, тогда не используйте f-strings
user3204459
2
Это именно то, что я искал, уклоняясь от 'fstr postpone ". Eval кажется не хуже, чем использование fstrings в целом, поскольку они, я полагаю, обладают одинаковой силой: f" {eval (' print (42) ')} "
user2692263
12

F-строка - это просто более краткий способ создания форматированной строки, заменяющей .format(**names)на f. Если вы не хотите, чтобы строка немедленно оценивалась таким образом, не делайте ее f-строкой. Сохраните его как обычный строковый литерал, а затем вызовите formatего позже, когда вы захотите выполнить интерполяцию, как вы это делали раньше.

Конечно, есть альтернатива eval.

template.txt:

f'Текущее имя - {name} '

Код:

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

Но тогда все , что вам удалось сделать , это заменить str.formatс eval, что, конечно , не стоит. Просто продолжайте использовать обычные строки при formatвызове.

TigerhawkT3
источник
3
Я действительно не вижу преимуществ в вашем фрагменте кода. Я имею в виду, вы всегда можете написать прямо The current name is {name}внутри template.txtфайла, а затем использовать print(template_a.format(name=name))(или .format(**locals())). Код примерно на 10 символов длиннее, но он не представляет никаких возможных проблем с безопасностью из-за eval.
Bakuriu
@Bakuriu - Да; как я уже сказал, хотя evalон позволяет нам писать f'{name}'и откладывать оценку nameдо желаемого, он уступает простому созданию обычной строки шаблона и последующему ее вызову format, как это уже делал OP.
TigerhawkT3
4
«Строка f - это просто более лаконичный способ создания отформатированной строки с заменой .format (** names) на f». Не совсем - у них другой синтаксис. У меня нет достаточно свежего python3 для проверки, но, например, я считаю, что f '{a + b}' работает, а '{a + b}'. Format (a = a, b = b) вызывает KeyError . .format (), вероятно, подходит во многих контекстах, но это не замена.
Philh
2
@philh Я думаю , я просто столкнулся примером , когда .formatне эквивалентен F-строка, которая может поддерживать вас комментарии: DNA = "TATTCGCGGAAAATATTTTGA"; fragment = f"{DNA[2:8]}"; failed_fragment = "{DNA[2:8]}".format(**locals()). Попытка создать failed_fragmentрезультаты в TypeError: string indices must be integers.
bli
12

Использование .format не является правильным ответом на этот вопрос. F-строки Python сильно отличаются от шаблонов str.format () ... они могут содержать код или другие дорогостоящие операции - отсюда и необходимость отсрочки.

Вот пример отложенного регистратора. При этом используется обычная преамбула logging.getLogger, но затем добавляются новые функции, интерпретирующие f-строку, только если уровень журнала правильный.

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)

Это дает возможность делать такие вещи, как: log.fdebug("{obj.dump()}").... без сброса объекта, если не включена отладка.

ИМХО: это должно было быть по умолчанию для f-строк, но теперь уже слишком поздно . Оценка F-строки может иметь массивные и непреднамеренные побочные эффекты, и если это произойдет в отложенном режиме, это изменит выполнение программы.

Чтобы сделать f-строки правильно отложенными, python потребуется какой-то способ явного переключения поведения. Может быть, использовать букву «г»? ;)

Было указано, что отложенное ведение журнала не должно приводить к сбою, если есть ошибка в преобразователе строк. Выше решение может сделать это , а также, изменить finally:к except:, и воткнуть log.exceptionтуда.

Эрик Аронести
источник
1
Полностью согласен с этим ответом. Этот вариант использования - это то, о чем я думал, когда искал этот вопрос.
justhalf
1
Это правильный ответ. Некоторое время: %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

То, что вы хотите, похоже, рассматривается как усовершенствование Python .

Между тем - из связанного обсуждения - следующее кажется разумным обходным путем, который не требует использования 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)

Выход:

The current name, number is 'foo', 41
The current name, number is 'bar', 42
Мартино
источник
7

вдохновленный ответом kadee , следующее можно использовать для определения класса 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)

это именно то, о чем спрашивал

user3204459
источник
4

Или, может быть, не используйте f-строки, просто отформатируйте:

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

В версии без имен:

fun = "The curent name is {}".format
names = ["foo", "bar"]
for name in names:
    print(fun(name))
msztolcman
источник
Это работает не во всех случаях. Пример: fun = "{DNA[2:8]}".format; DNA = "TATTCGCGGAAAATATTTTGA"; fun(DNA=DNA). ->TypeError: string indices must be integers
bli
Но он не работает и при обычном использовании, см. Ответ stackoverflow.com/questions/14072810/…
msztolcman
2

Как насчет:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

s.format(foo='Bar')
> 'Hi, Bar!'
Денис
источник
0

Предложение, использующее f-строки. Проведите оценку на логическом уровне, на котором происходит создание шаблона, и передайте его в качестве генератора. Вы можете раскрутить его в любой момент, когда захотите, используя 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  
Рон Лоухорн
источник