Не смотря на один из принципов Python, гласящий: «Должен существовать один — и, желательно, только один – очевидный способ сделать что-то», в нашем любимом языке есть аж четыре способа отформатировать строку. Так сложилось исторически.
Это второй урок цикла, посвящённого форматированию строк. В него входят:
- Строковый оператор форматирования
- Метод format()
- f-Строки
- Шаблонные строки
В данном уроке мы познакомимся с шаблонами строки.
Шаблоны строк – это инструмент, предоставляемый встроенным модулем string стандартной библиотеки Python. Для начала работы с ним необходимо импортировать класс Template:
from string import Template
Шаблоны находятся не в основном синтаксисе, а в модуле из-за того, что, как правило, используются не для повседневных задач, а для более специфических. Они имеют достаточно ограниченную функциональность, но могут тонко настраиваться.
Как это работает
Изначально шаблоны строк возникли как альтернатива строковому оператору форматирования для создания и обработки сложных шаблонов.
Давайте рассмотрим пример:
from string import Template
template_string = Template('Лучший язык программирования - $lang!')
prepared_string = template_string.substitute(lang='Python')
print(prepared_string)
# Вывод:
Лучший язык программирования - Python!
Здесь мы:
- Импортируем из модуля класс Template. Именно он и предоставляет всю магию.
- Определяем шаблон строки. «$lang» — идентификатор, вместо которого будет подставлено какое-то значение.
- Осуществляем подстановку методом «.substitute». Здесь мы указываем какое значение подставить вместо идентификатора lang=’Python’. Аргументы (их имена), которые мы передаём в «.substitute()», должны соответствовать идентификаторам, которые указаны в заполнителях строки шаблона.
- Выводим результат в консоль.
Можно представить этот механизм как команду: «если в этой строке есть идентификаторы, соответствующие именам аргументов, подставь вместо идентификаторов значения аргументов». При подстановке не учитывается тип значения передаваемого аргумента. Что бы там ни было, интерпретатор преобразует его в строку и вставит.
Поиск идентификатора и его замена значением осуществляется при помощи регулярного выражения, которое можно переопределить, но об этом позже. Узнать больше о регулярных выражениях Вы можете в нашем уроке на эту тему.
Строка Шаблона
Строкой шаблона может быть любая строка, содержащая правильные идентификаторы. Что означает «правильные»? По умолчанию существуют некоторые требования к идентификатору:
- Начинается с символа «$»
- Содержит буквы и/или цифры и/или знак нижнего подчёркивания. Не может начинаться с цифры. В целом, требования такие же, как к именам обычных переменных, за исключением того, что в идентификаторах шаблонов нельзя использовать кириллицу и другие не- ASCII символы.
- Идентификатор считается законченным тогда, когда встретился первый символ, не удовлетворяющий требованиям предыдущего пункта.
- Идентификатор может быть обрамлён фигурными скобками.
Допустимые идентификаторы:
$var, ${var}, $var_var, $vAr12345, $_1234
Недопустимые идентификаторы:
Var, $переменная, $1234, $var*, $(var)
Если Вам необходимо использовать символ «$» вне идентификатора, его необходимо экранировать, то есть поставить перед ним ещё один такой же символ, иначе Питон вернёт исключение:
from string import Template
from random import randint
template_string = Template('Хочу зарабатывать $zp$!')
prepared_string = template_string.substitute(zp=randint(10, 100)**randint(10, 100))
print(prepared_string)
# Вывод:
Traceback (most recent call last):
…
ValueError: Invalid placeholder in string: line 1, col 22
Process finished with exit code 1
from string import Template
from random import randint
template_string = Template('Хочу зарабатывать $zp$$!')
prepared_string = template_string.substitute(zp=randint(10, 100)**randint(10, 100))
print(prepared_string)
# Вывод:
Хочу зарабатывать 504857282956046106624$!
Можно даже так:
from string import Template
from random import randint
template_string = Template('Хочу зарабатывать $$$$$$$zp!')
prepared_string = template_string.substitute(zp=randint(10, 100)**randint(10, 100))
print(prepared_string)
# Вывод:
Хочу зарабатывать $$$1316217038422671360000000000000!
Теперь давайте обсудим для чего нужна возможность обрамлять идентификатор фигурными скобками. Вспомните: «Идентификатор считается законченным тогда, когда встретился первый символ, не удовлетворяющий требованиям предыдущего пункта». Таким символом, чаще всего, является пробел. Но, что, если у Вас, в том месте, где продолжается шаблон, должна располагаться обычная буква или цифра? Да, здесь и пригодятся фигурные скобки для отделения идентификатора:
from string import Template
from random import randint
template_string = Template('Хочу зарабатывать $zp000$$!')
prepared_string = template_string.substitute(zp=randint(10, 100))
print(prepared_string)
# Вывод:
Traceback (most recent call last):
…
KeyError: 'zp000'
Process finished with exit code 1
from string import Template
from random import randint
template_string = Template('Хочу зарабатывать ${zp}000$$!')
prepared_string = template_string.substitute(zp=randint(10, 100))
print(prepared_string)
# Вывод:
Хочу зарабатывать 24000$!
Так же Вам могут понадобиться манипуляции с частями слова:
from string import Template
def semantic_reverse(word):
template_string = Template('${word}less')
prepared_string = template_string.substitute(word=word)
print(prepared_string)
semantic_reverse('brain')
semantic_reverse('limit')
# Вывод:
brainless
limitless
Вот ещё пример, уже близкий к реальной задаче. Допустим, нам надо динамически создавать путь к директории в файловой системе:
from string import Template
def create_path(*crumbs):
result_path = ''
template_string = Template('$crumb/')
for crumb in crumbs:
prepared_string = template_string.substitute(crumb=crumb)
result_path += prepared_string
return result_path
my_path = create_path('C', 'home', 'dir')
print(my_path)
# Вывод:
C/home/dir/
Поскольку косая черта не является допустимым символом идентификатора, скрипт работает так, как мы того хотели.
Но, если встанет подобная задача, но разделителем будет допустимый символ, мы столкнёмся с проблемой:
from string import Template
def create_file_name(*crumbs):
file_name = ''
template_string = Template('$crumb_')
for crumb in crumbs:
prepared_string = template_string.substitute(crumb=crumb)
file_name += prepared_string
file_name = file_name[:-1]
file_name += '.xml'
return file_name
my_path = create_file_name('2021', '12', '31')
print(my_path)
# Вывод:
Traceback (most recent call last):
…
KeyError: 'crumb_'
На помощь вновь приходят фигурные скобки:
from string import Template
def create_file_name(*crumbs):
file_name = ''
template_string = Template('${crumb}_')
for crumb in crumbs:
prepared_string = template_string.substitute(crumb=crumb)
file_name += prepared_string
file_name = file_name[:-1]
file_name += '.xml'
return file_name
my_path = create_file_name('2021', '12', '31')
print(my_path)
# Вывод:
2021_12_31.xml
Да, теперь всё верно. Это из-за того, что фигурные скобки правильно ограничивают идентификаторы от нижнего подчёркивания.
Сама строка шаблона хранится в атрибуте template экземпляра Template:
from string import Template
template_string = Template('Какая-то строка с идентификатором $crumb')
print(template_string.template)
# Вывод:
Какая-то строка с идентификатором $crumb
Мы можем менять шаблон объекта, но это сложно назвать хорошим стилем программирования:
from string import Template
template_string = Template('Какая-то строка с идентификатором $crumb')
print(template_string.template)
print(template_string.substitute(crumb=12345))
template_string.template = 'Какая-то новая строка с идентификатором $var'
print(template_string.template)
print(template_string.substitute(var='"здесь был идентификатор"'))
# Вывод:
Какая-то строка с идентификатором $crumb
Какая-то строка с идентификатором 12345
Какая-то новая строка с идентификатором $var
Какая-то новая строка с идентификатором "здесь был идентификатор"
Метод substitute()
Метод .substitute() осуществляет подстановку значений вместо идентификаторов. Для этого он сопоставляет имена аргументов и имена идентификаторов. Идентификаторов, естественно, может быть несколько:
from string import Template
template_string = Template('$a и $b сидели на трубе')
print(template_string.substitute(a='Python', b='С++'))
# Вывод:
Python и С++ сидели на трубе
Кроме именованных аргументов, методу . substitute() можно передать словарь. Для этого необходимо применить распаковку словаря (оператор «**» перед именем словаря):
from string import Template
template_string = Template('$a и $b сидели на трубе')
my_dict = {'a': 'Python', 'b': 'С++'}
print(template_string.substitute(**my_dict))
# Вывод:
Python и С++ сидели на трубе
Распространенные ошибки
Метод .substitute() строг и не прощает ошибок. Если Вы в шаблоне указали больше идентификаторов, чем передаёте аргументов, вернётся ошибка KeyError:
from string import Template
template_string = Template('$a и $b сидели на трубе')
my_dict = {'a': 'Python',}
print(template_string.substitute(**my_dict))
# Вывод:
Traceback (most recent call last):
…
KeyError: 'b'
Process finished with exit code 1
То же самое произойдёт если передать аргумент, имя которого не соответствует идентификатору.
Если мы укажем недопустимый идентификатор, то получим исключение ValueError, оповещающее что заполнитель неверен.
Метод safe_substitute()
Метод .safe_substitute() – менее строгий аналог . substitute(). Он делает всё то же самое, но не поднимает описанные выше исключения: Если аргументов не хватает или их имена не соответствуют идентификаторам, метод .safe_substitute() просто не выполнит замену идентификатора на значение аргумента и вернёт строку «как есть». Проверим на приведённом выше примере:
from string import Template
template_string = Template('$a и $b сидели на трубе')
my_dict = {'a': 'Python',}
print(template_string.safe_substitute(**my_dict))
# Вывод:
Python и $b сидели на трубе
Настройка класса Template
Как и от любого класса в Питоне, от Template можно наследоваться. Это означает, что мы можем переопределить его атрибуты. Именно здесь скрыты самые интересные возможности этого инструмента. И так, наследуемся:
from string import Template
class NewTemplate(Template):
pass
Переопределяем разделитель
Атрибут .delimiter содержит символ, используемый в качестве начального символа идентификатора:
from string import Template
template_string = Template('$a и $b сидели на трубе')
print(template_string.delimiter)
# Вывод:
$
Теперь попробуем его переопределить:
from string import Template
class NewTemplate(Template):
delimiter = 'Подставляю, значится, это:'
template_string = Template('Подставляю, значится, это:a и $b сидели на трубе')
print(template_string.safe_substitute(a='Python'))
template_string = NewTemplate('Подставляю, значится, это:a и $b сидели на трубе')
print(template_string.safe_substitute(a='Python'))
# Вывод:
Подставляю, значится, это:a и $b сидели на трубе
Python и $b сидели на трубе
Теперь класс NewTemplate можно использовать так же, как класс Template, но в роли разделителя будет выступать не «$», а «’Подставляю, значится, это:».
Зачем это нужно? Представьте, что у Вас в шаблоне есть множество символов «$», которые должны быть частью шаблона, а не идентификатора. Если не переопределять разделитель, придётся вручную экранировать каждый из них. Ещё хуже ситуация становится, если программа получает эту строку из внешнего источника. Другой вариант, когда удобно переопределять разделитель, это шаблоны, в которых уже есть разделители но для другого синтаксиса. К примеру, у Вас есть текст запроса к базе данных на T-SQL и в нём есть параметры запроса. Очень просто:
from string import Template
class NewTemplate(Template):
delimiter = '@'
qwery = 'select 1 from my_shema.my_table where my_table.id = @id'
template_string = NewTemplate(qwery)
print(template_string.safe_substitute(id=13))
# Вывод:
select 1 from my_shema.my_table where my_table.id = 13
Переопределяем маску идентификатора
Атрибут idpattern — это регулярное выражение, применяемое для проверки тела идентификатора, указанного в строке шаблона:
from string import Template
template_string = Template('$a и $b сидели на трубе')
print(template_string.idpattern)
# Вывод:
(?a:[_a-z][_a-z0-9]*)
Естественно, мы можем переопределить и этот атрибут. Как правило, такое переопределение будет вносить более строгие правила именования идентификатора. Интересный пример:
from string import Template
class NewTemplate(Template):
delimiter = ' '
idpattern = 'Оля'
qwery = 'Села Оля на пенёк, съела Оля пирожок'
template_string = NewTemplate(qwery)
print(template_string.safe_substitute())
print(template_string.safe_substitute(Оля=' Лариса Петровна'))
# Вывод:
Села Оля на пенёк, съела Оля пирожок
Села Лариса Петровна на пенёк, съела Лариса Петровна пирожок
Атрибут pattern
Если переопределения атрибутов delimiter и idpattern недостаточно, можно переопределить атрибут pattern.
Для этого Вам нужно предоставить регулярное выражение с четырьмя именованными группами:
- escape — соответствует последовательности для разделителя
- named — соответствует допустимому идентификатору как в $identifier и не должна включать разделитель.
- braced — эта группа соответствует закрытому в скобки имени, как в ${identifier}. Она не должна включать разделитель escaped или фигурные скобки.
- invalid — эта группа соответствует любому другому шаблону разделителя (обычно одиночному разделителю), и она должна появляться последней.
Вот как можно узнать текущий паттерн:
from string import Template
class NewTemplate(Template):
delimiter = ' '
idpattern = 'Оля'
qwery = 'Села Оля на пенёк, съела Оля пирожок'
template_string = NewTemplate(qwery)
print(template_string.pattern)
print(template_string.pattern.pattern)
# Вывод:
re.compile('\n \\ (?:\n (?P<escaped>\\ ) | # Escape sequence of two delimiters\n (?P<named>Оля) | # delimiter and a Python identifier\n {(?P<braced>Ол, re.IGNORECASE|re.VERBOSE)
\ (?:
(?P<escaped>\ ) | # Escape sequence of two delimiters
(?P<named>Оля) | # delimiter and a Python identifier
{(?P<braced>Оля)} | # delimiter and a braced identifier
(?P<invalid>) # Other ill-formed delimiter exprs
)