Строковые подстановки с использованием шаблонов в Python

Введение

Строковый модуль имеет класс Template, который позволяет делать замены в строке с использованием объекта сопоставления, например:

>>> string.Template('var is $var').substitute({'var': 1})
'var is 1'

Альтернативный метод может вызвать исключение KeyError, если делается попытка заменить элемент, отсутствующий в сопоставлении, например

>>> string.Template('var is $var and foo is $foo').substitute({'var': 1})
KeyError: 'foo'

или может вызвать значение ValueError, если строка шаблона недействительна, например. он содержит символ $, за которым следует пробел:

>>> string.Template('$ var is $var').substitute({'var': 1})
ValueError: Invalid placeholder in string: line 1, col 1

Проблема

Учитывая строку шаблона и сопоставление, я хочу определить, будут ли заменены все владельцы мест в шаблоне. Для этого я попытался бы сделать замену и поймать любое исключение KeyError:

def check_substitution(template, mapping):
    try:
        string.Template(template).substitute(mapping)
    except KeyError:
        return False
    except ValueError:
        pass
    return True

Но это не работает, потому что, если шаблон недействителен и ValueError поднят, последующие KeyErrors не пойманы:

>>> check_substitution('var is $var and foo is $foo', {'var': 1})
False
>>> check_substitution('$ var is $var and foo is $foo', {'var': 1})
True

но меня не интересует ValueErrors. Итак, каков был бы правильный подход к этой проблеме?

Ответ 1

Документы говорят, что вы можете заменить шаблон, если он содержит все необходимые именованные группы:

import re
from string import Template


class TemplateIgnoreInvalid(Template):
    # override pattern to make sure `invalid` never matches
    pattern = r"""
    %(delim)s(?:
      (?P<escaped>%(delim)s) |   # Escape sequence of two delimiters
      (?P<named>%(id)s)      |   # delimiter and a Python identifier
      {(?P<braced>%(id)s)}   |   # delimiter and a braced identifier
      (?P<invalid>^$)            # never matches (the regex is not multilined)
    )
    """ % dict(delim=re.escape(Template.delimiter), id=Template.idpattern)


def check_substitution(template, **mapping):
    try:
        TemplateIgnoreInvalid(template).substitute(mapping)
    except KeyError:
        return False
    else:
        return True

Испытания

f = check_substitution
assert f('var is $var', var=1)
assert f('$ var is $var', var=1)
assert     f('var is $var and foo is $foo', var=1, foo=2)
assert not f('var is $var and foo is $foo', var=1)
assert     f('$ var is $var and foo is $foo', var=1, foo=2)
assert not f('$ var is $var and foo is $foo', var=1)
# support all invalid patterns
assert f('var is $var and foo is ${foo', var=1)
assert f('var is $var and foo is ${foo', var=1, foo=2) #NOTE: problematic API
assert     f('var is $var and foo is ${foo and ${baz}', var=1, baz=3)
assert not f('var is $var and foo is ${foo and ${baz}', var=1)

Он работает для всех недопустимых вхождений разделителя ($).

Примеры показывают, что игнорирование недействительных шаблонов скрывает простые опечатки в шаблоне, поэтому это не хороший API.

Ответ 2

Это быстрое исправление (с использованием рекурсии):

def check_substitution(tem, m):
    try:
        string.Template(tem).substitute(m)
    except KeyError:
        return False
    except ValueError:
        return check_substitution(tem.replace('$ ', '$'), m) #strip spaces after $
    return True

Я знаю, что это займет больше времени, если между $ и var больше одного пробела, поэтому вы можете улучшить его, используя регулярное выражение.

ИЗМЕНИТЬ

escaping $ в $$ имеет больше смысла [Спасибо @Pedro], чтобы вы могли поймать ValueError этим утверждением:

return check_substitution(tem.replace('$ ', '$$ '), m) #escaping $ by $$

Ответ 3

Python не будет выполнять замену строк по нескольким строкам

Если у вас есть эта строка

criterion = """
    <criteria>
    <order>{order}</order>
      <body><![CDATA[{code}]]></body>
    </criteria>
"""

criterion.format(dict(order="1",code="Hello")

приводит к:

KeyError: 'order'

Решение состоит в использовании модуля string.Template

from string import Template

criterion = """
    <criteria>
    <order>$order</order>
      <body><![CDATA[$code]]></body>
    </criteria>
"""

Template(criterion).substitute(dict(order="1",code="hello")

ПРИМЕЧАНИЕ: вы должны префикс ключевых слов с помощью $not wrap them в {}

:

 <criteria>
    <order>1</order>
      <body><![CDATA[hello]]></body>
    </criteria>

Полные документы: https://docs.python.org/2/library/string.html#template-strings