Шаблонные строки Python

Основы

Не смотря на один из принципов Python, гласящий: «Должен существовать один — и, желательно, только один – очевидный способ сделать что-то», в нашем любимом языке есть аж четыре способа отформатировать строку. Так сложилось исторически.
Это второй урок цикла, посвящённого форматированию строк. В него входят:

  1. Строковый оператор форматирования
  2. Метод format()
  3. f-Строки 
  4. Шаблонные строки

В данном уроке мы познакомимся с шаблонами строки.

Шаблоны строк – это инструмент, предоставляемый встроенным модулем string стандартной библиотеки Python. Для начала работы с ним необходимо импортировать класс 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.
Для этого Вам нужно предоставить регулярное выражение с четырьмя именованными группами:

  1. escape — соответствует последовательности для разделителя
  2. named — соответствует допустимому идентификатору как в $identifier и не должна включать разделитель.
  3. braced — эта группа соответствует закрытому в скобки имени, как в ${identifier}. Она не должна включать разделитель escaped или фигурные скобки.
  4. 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

)

Оцените статью
О Python на русском языке
Добавить комментарий

Adblock
detector