Как стать автором
Обновить

Учим старую собаку новым трюкам или как я научился любить str.format и отказался от %

Время на прочтение7 мин
Количество просмотров63K
Автор оригинала: Reuven Lerner
Предлагаю вниманию читателей хабры и поклонникам python перевод довольно объемной статьи о форматировании строк. Сказка будет правдивой и намек в ней будет на то, что консерваторам стоит иногда рассматривать что-то новое, даже если привычка упорно сопротивляется.

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

Жду замечания по ошибкам в оформлении и опечаткам в личку — с меня традиционные хабраплюшки.


Далее слова автора оригинальной статьи:

Я уже много лет пишу на python. Но в самом начале этого пути мне было интересно узнать как форматировать строки в стиле Perl. Напомню, что Perl (и многие интерпретаторы командной строки в Unix) поддерживают два типа строковых литералов — с одинарными кавычками (когда строка выводится как есть), и двойными где на место переменных подставляются их значения. В Perl, например, можно написать что то вроде:

$name = 'Reuven';
print "Hello, $name\n";

И программа, соответственно, напишет «Hello, Reuven».

Строковые литералы в python не зависят от типа кавычек и переменные в них никогда не разворачиваются в свои значения. Чтобы этого добиться традиционно использовался оператор % для строк. В этом контексте оператор смотрит на строку слева от себя и подсчитывает сколько значений нужно заменить на значения соответствующих переменных справа от себя. Результатом операции является новая строка со вставленными на место плейсхолдеров значениями переменных. Например:

>>> name = 'Reuven'
>>> "Hello, %s" % name

'Hello, Reuven'

Этот код на python вполне себе работает и выводит персонализированное приветствие. Так, несмотря на мою многолетнюю практику с python — я был вполне удовлетворен применением этого синтаксиса. Да, он не очень приятный и нет, я никогда не держал в памяти гору модификаторов printf, которые влияют на форматирование. В смысле я всегда использовал модификатор 's' (выводить как строку) и мне было достаточно того, что python неявно приводил аргументы к строке.

Но в данный момент факт, что синтаксис % подлежит списанию или, по крайней мере, объявлен устаревшим. В списке рассылки python-dev есть замечание, что в ветке 2.x он проживет минимум до 2022 года, но ничего не сказано про ветку 3.x, так что поддержка этого синтаксиса будет скоро удалена и применять его нежелательно. На смену ему пришел метод str.format.

В своих уроках по python я всегда упоминал о str.format, но в конкретных примерах чаще все полагался все-таки на %. Я даже рекомендовал студентам использовать % так как лично мне он казался намного проще.

Но стойкое ощущение того, что, я делаю что-то не так и, возможно, даже ввожу в заблуждение своих студентов подвигло меня поближе изучить str.format. В ходе исследования, я пришел к следующим выводам: 1) Он ничуть не сложнее % и даже проще в некоторых вариантах применения; 2) Я никогда не применял возможности str.format в полной мере, а они очень удобные, несмотря на некоторое время необходимое для их изучения.

Начнем с простейшего. Скажем кому-нибудь «Good morning», причем обратимся по имени и фамилии, предполагая что они сохранены в переменных «first» и «last». По-старому мы сделали бы так:

>>> first = 'Reuven'
>>> last = 'Lerner'
>>> "Good morning, %s %s" % (first, last)

'Good morning, Reuven Lerner'

Даже в таком примере мы сталкиваемся с одной из проблем %-синтаксиса — у нас теперь две переменных, и чтобы использовать их обе нам нужно сделать из них кортеж. С точки зрения python это логично, но, уверяю вас, очень многих студентов это очень удивляет.

Как этот пример будет выглядеть в случае str.format? Довольно похоже:

>>> "Good morning, {} {}".format(first, last)

'Good morning, Reuven Lerner'

Прошу обратить внимание, что мы немного поменяли принцип. Теперь это не бинарный оператор над строками, а метод объекта строка, принимающий ряд параметров. Это логично и более консистентно. Для тех же студентов оператор % в моих примерах выглядел как дополнение к print, а не операция над строками. Нотация с ".format" после строки делает более очевидным факт того, что это метод относящийся именно к этой строке.

Как вы уже наверняка знаете, вхождения “{} {}” в строке говорят что str.format должен принимать два параметра, значения которых будут вставлены в строку в том порядке, в котором они будут переданы в метод. Аргумента два, поэтому в строке должно быть два вхождения {}. Это немного сложнее понять, так как фигурные скобочки в Python намекают людям на словари и пустые скобочки выглядят не очень приятно. Но это ладно, я вполне могу с этим жить и принял это достаточно легко.

Момент, в котором str.format показывает первое преимущество над % — это при необходимости использования параметров в обратном порядке. На самом деле, с %s этого вообще никак не достичь. Невозможно также использовать значение одной переменной несколько раз. При использовании str.format мы вполне можем поменять последовательность подстановки:

>>> "Good morning, {1} {0}".format(first, last)

'Good morning, Lerner Reuven'

Обратите внимание, что если бы я использовал пустые скобочки “{} {}”, то подстановка произошла бы в том же порядке, в каком передаются в метод параметры. Можно представить себе параметры как индексируемую с нуля последовательность и если я хочу поменять порядок следования, то просто проставляю в фигурных скобочках нужные индексы этой последовательности. Самый первый наш пример с str.format можно записать и так:

>>> "Good morning, {0} {1}".format(first, last)

'Good morning, Reuven Lerner'

Заметим, что явно указав индексы, мы уже не можем положиться на автоматическую индексацию.

Разумеется, можно использовать последовательность и из списка, воспользовавшись оператором *:

>>> names = ('Reuven', 'Lerner')
>>> "Good morning, {} {}".format(*names)

'Good morning, Reuven Lerner'

Можно использовать и именованные аргументы:

>>> "Good morning, {first} {last}".format(first='Reuven', last='Lerner')

'Good morning, Reuven Lerner'

Этот вариант мне особенно нравится. Именованные параметры более явные (если у них хорошие имена), и применение {first} и {last} достаточно читабельно — особенно в сравнении с %(first)s, которое необходимо с оператором %

Именованные параметры можно, также, развернуть из словаря, используя оператор **:

>>> person = {'first':'Reuven', 'last':'Lerner'}
>>> "Good morning, {first} {last}".format(**person)

'Good morning, Reuven Lerner'

Я описал все это своим студентам и был достаточно удивлен тем насколько комфортнее им живется с таким синтаксисом. Да и самому стало приятнее работать.

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

>>> person = {'first':'Reuven', 'last':'Lerner'}
>>> "Good {0}, {first} {last}".format('morning', **person)

'Good morning, Reuven Lerner'


Я предупредил.

Чего может не хватать в str.format, так это… гм… форматирования. Плохая новость — в str.format совершенно другие правила определения того как форматировать вывод. Хорошая новость — эти правила достаточно несложно изучить и понять.

Снова начнем с простого: если нужно вывести строку заданной длины, то после имени переменной добавляем двоеточие (:) и затем количество символов. Так, чтобы вывести мое имя и дополнить его до десяти символов пробелами я должен делать так:

>>> "Your name is {name:10}".format(name="Reuven")

'Your name is Reuven    '

(Обратите внимание что строка дополнена пробелами после имени.)

Если нужно задать выравнивание по правой стороне блока — используется знак > между: и числом:

>>> "Your name is {name:>10}".format(name="Reuven")

'Your name is     Reuven'

И да, можно явно указать что я хочу выравнивания по левой стороне с помощью знака <
Если нужно вывести значение в центре блока, то вместо < и > используется символ ^:

>>> "Your name is {name:*^10}".format(name="Reuven")

'Your name is **Reuven**'

С текстом более менее понятно, но что насчет чисел? Лично мне было трудно предположить как это будет работать, но все оказалось достаточно прямолинейно. Для простого вывода чисел используем синтаксис похожий на строки:

>>> "The price is ${number}.".format(number=123)

'The price is $123.'

Но для чисел применяется большее количество модификаторов, чем для строк. Например, чтобы вывести число в двоичном виде добавляем модификатор «b», если в шестнадцатеричном — модификатор «x»:

>>> "The price is ${number:b}.".format(number=5)

'The price is $101.'

>>> "The price is ${number:x}.".format(number=123)

'The price is $7b.'


Разумеется, запись числа можно дополнить лидирующими нулями:

>>> "Your call is important to us. You are call #{number:05}.".format(number=123)

'Your call is important to us. You are call #00123.'

Заметим, что внутри {} нельзя использовать исполняемый python-код — вместо этого предлагается простенький микроязык отдельный и отличающийся от python в целом. Есть и небольшие исключения. Во-первых можно получить значения атрибутов/свойств через точку, во-вторых получить значение объекта по индексу, используя [].

Например:

>>> class Foo(object):
        def __init__(self):
        self.x = 100
>>> f = Foo()
>>> 'Your number is {o.x}'.format(o=f)

'Your number is 100'n

Мы получили атрибут «х» объекта «f». Этот объект доступен по имени «o» внутри строки. Получить атрибут можно, а вот выполнить его — нет:

>>> "Your name is {name.upper()}".format(name="Reuven")

AttributeError: 'str' object has no attribute 'upper()'

Я пытался выполнить “name.upper()”, предполагая, что будет вызван соответствующий метод, но python не разрешает выполнять код в этом месте и расценивает «upper()» как атрибут вместе со скобками. Без скобок вы получите просто строковое представление функции/метода:

>>> "Your name is {name.upper}".format(name="Reuven")

'Your name is <built-in method upper of str object at 0x1028bf2a0>'

С помощью квадратных скобок можно взять элемент итерируемого объекта (списка, строки) по индексу. Но операции разрезания (slice) не поддерживаются:

>>> "Your favorite number is {n[3]}.".format(n=numbers)

'Your favorite number is 3.'

Но:

>>> "Your favorite numbers are {n[2:4]}.".format(n=numbers)

ValueError: Missing ']' in format string

Можно использовать [] и для получения записей в словаре по имени, но имя вводится без кавычек:

>>> person = {'first':'Reuven', 'last':'Lerner'}
>>> "Your name is {p[first]}.".format(p=person)

'Your name is Reuven.'

При попытке использовать кавычки получим исключение…

>>> "Your name is {p['first']}.".format(p=person)

KeyError: "'first'"

Здесь приведены не все варианты использования str.format — на деле для каждого типа есть спецификация правил форматирования. Например, опция точности для чисел с плавающей запятой недоступна для строк.

Можно даже добавить собственные правила форматирования для объектов ваших классов так, что у них будет особый способ вывода и модификаторы для его настройки.

Если есть желание изучить эту тему подробнее — стоит начать с PEP 3101, где описан str.format. Могу, также, порекомендовать презентацию Эрика Смита с достаточно хорошим саммари по этой теме. Есть и хорошие примеры о том как перейти от использования % к str.format в документации python

Надеюсь, вам понравилось!

P… S.: Автор оригинальной статьи замерил производительность str.format и %. Пришел к выводу, что % быстрее

P.P.S.: По словам Андрея Светлова svetlov (словам которого можно доверять в силу его вхожести в команду разработки python) — синтаксис % не будет убран из python 3.x минимум ближайшие 20 лет
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А как вы форматируете строки?
34.67% Привык и использую только %286
47.39% Давно уже перешел на str.format391
17.94% Зачем это все? Конкатенация плюсом наше все!148
Проголосовали 825 пользователей. Воздержались 164 пользователя.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 76: ↑69 и ↓7+62
Комментарии39

Публикации

Истории

Работа

Data Scientist
71 вакансия
Python разработчик
110 вакансий

Ближайшие события