Форматированные строковые литералы, которые ещё называют f-строками (f-strings), появились довольно давно, в Python 3.6. Поэтому все знают о том, что это такое, и о том, как ими пользоваться. Правда, f-строки обладают кое-какими полезными возможностями, некоторыми особенностями, о которых кто-нибудь может и не знать. Разберёмся с некоторыми интересными возможностями f-строк, которые могут оказаться очень кстати в повседневной работе Python-программиста.

Форматирование даты и времени

Форматирование чисел средствами f-строк — это обычное дело. А вы знали, что с их помощью можно ещё форматировать значения, представляющие даты и временные метки?

import datetime
today = datetime.datetime.today()
print(f"{today:%Y-%m-%d}")
# 2022-03-11
print(f"{today:%Y}")
# 2022

С помощью f-строк можно форматировать дату и время так, как если бы для этого использовался бы метод datetime.strftime. Это особенно приятно, когда понимаешь, что тут имеется больше возможностей форматирования значений, чем те немногие, которые упомянуты в документации. Так, Python-метод strftime поддерживает, кроме прочего, все способы форматирования значений, поддерживаемые его базовой реализацией на C. Эти возможности могут зависеть от платформы, именно поэтому в документации они не упоминаются. Но получается, что этими возможностями по форматированию значений, всё равно, можно воспользоваться. Например, можно применить спецификатор формата %F, являющийся эквивалентом %Y-%m-%d, или спецификатор %T, аналогичный %H:%M:%S. Стоит ещё упомянуть и о спецификаторах формата %x и %X, представляющих собой, соответственно, принятые в используемом языковом стандарте способы представления даты и времени. Использование этих возможностей форматирования значений, конечно, не ограничивается только f-строками. Полный список спецификаторов формата даты и времени можно найти в Linux-справке по strftime.

Имена переменных и отладка

Функционал f-строк сравнительно недавно (начиная с Python 3.8) дополнен возможностями по выводу имён переменных вместе с их значениями:

x = 10
y = 25
print(f"x = {x}, y = {y}")
# x = 10, y = 25
print(f"{x = }, {y = }")  # Лучше! (3.8+)
# x = 10, y = 25

print(f"{x = :.3f}")
# x = 10.000

Эта возможность называется «отладкой» («debugging»), её можно применять вместе с другими модификаторами. Она, кроме того, сохраняет пробелы, поэтому при обработке конструкций вида f»{x = }» и f»{x=}» получатся разные строки.

Методы __repr__ и __str__

Для формирования строковых представлений экземпляров классов по умолчанию используется метод __str__. Но если вместо этого метода нужно применить метод __repr__ — можно воспользоваться флагом преобразования !r:

class User:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

    def __repr__(self):
        return f"User's name is: {self.first_name} {self.last_name}"

user = User("John", "Doe")
print(f"{user}")
# John Doe
print(f"{user!r}")
# User's name is: John Doe

Тут, внутри f-строки, можно было бы просто вызвать repr(some_var), но использование флага преобразования — это образец приятного стандартного и краткого решения подобной задачи.

Отличная производительность

За некие мощные возможности чего-либо и за «синтаксический сахар» часто приходится платить производительностью. Но в случае с f-строками это не так:

# python -m timeit -s 'x, y = "Hello", "World"' 'f"{x} {y}"'
from string import Template

x, y = "Hello", "World"

print(f"{x} {y}")  # 39.6 nsec per loop - Быстро!
print(x + " " + y)  # 43.5 nsec per loop
print(" ".join((x, y)))  # 58.1 nsec per loop
print("%s %s" % (x, y))  # 103 nsec per loop
print("{} {}".format(x, y))  # 141 nsec per loop
print(Template("$x $y").substitute(x=x, y=y))  # 1.24 usec per loop - Медленно!

Вышеприведённый код протестирован с помощью модуля timeit (python -m timeit -s 'x, y = «Hello», «World»' 'f»{x} {y}»'). Как видите, f-строки оказались самым быстрым из всех механизмов форматирования данных, которые даёт нам Python. Поэтому, даже если вы предпочитаете пользоваться другими средствами форматирования строк, рассмотреть возможность перехода на f-строки стоит хотя бы ради повышения производительности.

Вся сила мини-языка спецификаций форматирования

F-строки поддерживают мини-язык спецификаций форматирования Python. Поэтому в модификаторы, используемые в f-строках, можно внедрить множество операций форматирования данных:

text = "hello world"

# Центрирование текста:
print(f"{text:^15}")
# '  hello world  '

number = 1234567890
# Установка разделителя групп разрядов
print(f"{number:,}")
# 1,234,567,890

number = 123
# Добавление начальных нулей
print(f"{number:08}")
# 00000123

Мини-язык форматирования Python включает в себя гораздо больше, чем конструкции, рассчитанные на форматирование чисел и дат. Этот язык, кроме прочего, позволяет выравнивать или центрировать текст, добавлять к строкам начальные нули или пробелы, задавать разделители групп разрядов. Всем этим, конечно, можно пользоваться не только в f-строках, но и при применении других способов форматирования данных.

Вложенные f-строки

Если чьи-то нужды по форматированию данных не удаётся удовлетворить с помощью простых f-строк, можно прибегнуть к f-строкам, вложенным друг в друга:

number = 254.3463
print(f"{f'${number:.3f}':>10s}")
# '  $254.346'

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

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

import decimal
width = 8
precision = 3
value = decimal.Decimal("42.12345")
print(f"output: {value:{width}.{precision}}")
# 'output:     42.1'

Условное форматирование

Взяв за основу предыдущий пример с вложенными f-строками, можно пойти немного дальше и воспользоваться во внутренних f-строках тернарными условными операторами:

import decimal
value = decimal.Decimal("42.12345")
print(f'Result: {value:{"4.3" if value < 100 else "8.3"}}')
# Result: 42.1
value = decimal.Decimal("142.12345")
print(f'Result: {value:{"4.2" if value < 100 else "8.3"}}')
# Result:      142

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

Лямбда-выражения

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

print(f"{(lambda x: x**2)(3)}")
# 9

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

Итоги

Как видите, f-строки — это, и правда, весьма мощный механизм. Они обладают гораздо большими возможностями, чем думает большинство программистов. Основная часть этих «неизвестных» возможностей, правда, описана в документации по Python. Поэтому рекомендую читать документацию, причём — не только по f-строкам, но и по другим используемым вами модулям или возможностям Python. Углубление в документацию часто помогает обнаружить какие-то очень полезные вещи, которые не найти даже зарывшись в Stack Overflow.

О, а приходите к нам работать? ???? ????

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

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

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде.

Комментарии (15)


  1. Maxim_Evstigneev
    04.07.2022 13:46
    +3

    думаю в задачах генерации массивов текста фичи крайне полезны


  1. lgorSL
    04.07.2022 15:23
    +7

    Ещё f-строки можно использовать в ассертах и получать понятные сообщения об ошибках.

    Assert a==b f"{a} ! = {b}"


    1. maslyaev
      05.07.2022 10:04

      Эээээ.... это в какой версии?


      1. PashaWNN
        05.07.2022 10:55
        +1

        Это в любой версии с поддержкой f-строк, так как подобный синтаксис:

        assert foo == bar, 'spam'
        

        Давно существует и предназначен для вывода кастомного сообщения в AssertionError в случае, если условие ложно.


  1. kuza2000
    04.07.2022 15:25
    +1

    Про test= понравилось, часто такое пишу)


  1. osmanpasha
    04.07.2022 21:32

    Интересно, почему они такие быстрые, даже быстрее, чем конкатенация. Что-то тут не так


    1. gchebanov
      04.07.2022 21:47
      +2

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


    1. mr-quokka
      05.07.2022 12:43
      -1

      В статье и правда нереалистичные цифры, все принты работают за примерно тоже самое время. Модератор, если можете, удалите мой другой комментарий

      import timeit
      
      setup = """
      x = 3
      y = 4
      """
      
      funcs = [
        'print(f"{x} {y}")',
        'print(str(x) + " " + str(y))',
        "print(str(x) + str(y))",
        'print("{} {}".format(x, y))',
      ]
      
      results = {}
      
      for item in funcs:
        results[item] = timeit.Timer(item, setup=setup).repeat(7, 1000)
      
      for func, value in results.items():
        print("min = {}, max = {}, {}".format(min(value), max(value), func))
      


      Результаты

      min = 0.0015034589887363836, max = 0.0016057030006777495, print(f"{x} {y}")
      min = 0.001643661002162844, max = 0.0017650749941822141, print(str(x) + " " + str(y))
      min = 0.0015372109919553623, max = 0.0016501189966220409, print(str(x) + str(y))
      min = 0.0015749409940326586, max = 0.0016585019911872223, print("{} {}".format(x, y))


      1. sebres
        06.07.2022 11:56

        Не нужно измерять print, читайте внимательнее, в статье написано как оно измерялось...

        >>> from timeit import timeit
        >>> for s in ('f"{x} {y}"', '"{} {}".format(x, y)' ,'x+" "+y', '"%s %s" % (x, y)'):
                print(timeit(s, 'x, y = "Hello", "World"', number=1000000), s)
        0.1258036000000402 f"{x} {y}"
        0.4175639999999703 "{} {}".format(x, y)
        0.2175986999999395 x+" "+y
        0.3835477999999739 "%s %s" % (x, y)


        1. mr-quokka
          07.07.2022 07:37

          Проверил, результат зависит от арументов 3 и 4

          min = 0.00010221399134024978, max = 0.00018698800704441965, f"{x} {y}"
          min = 0.00018588098464533687, max = 0.00024034999660216272, str(x) + " " + str(y)
          min = 0.00015182801871560514, max = 0.0001902490039356053, str(x) + str(y)
          min = 0.00013922399375587702, max = 0.00016092602163553238, "{} {}".format(x, y)


          Для 10 и 25 (как в статье)
          min = 0.0001049069978762418, max = 0.00012914801482111216, f"{x} {y}"
          min = 0.0001870139967650175, max = 0.00018797098891809583, str(x) + " " + str(y)
          min = 0.00016022301861084998, max = 0.00016407601651735604, str(x) + str(y)
          min = 0.00014531699707731605, max = 0.00014632698730565608, "{} {}".format(x, y)


          Начиная со второго символа начинается прирост, был не прав


  1. Red_Nose
    04.07.2022 22:09
    -7

    Очень полезно было-бы сравнение со "старичками" - всякими разными С/Perl/Java/Cobol/...

    Ведь питон - "удобный", надо реально удобство показывать !

    P.S. может не очень внимательно читал (яжпаграмист, доку читаю когда больно :) - кроме древнего сишного printf и перловских понтов ничего не увидел :(

    P.P.S. сишный printf - КРАЙНЕ сложная функция. И в "удобный" питон оно переползло как есть - зачем заморачиваться :)


  1. KivApple
    04.07.2022 23:05
    +3

    Мне не нравится, что внутри выражений внутри f-строк кавычки трактуются как конец строки. В некоторых других языках внутри {} полностью новый контекст парсинга и кавычки не закрывают внешнюю строку. Приходится менять тип кавычек.


    1. seonn
      05.07.2022 08:27
      +1

      вы можете использовать \ для экранировании кавычек, так же как и в простых строках
      var = "world"
      print(f"he\"llo = {var}")
      >>> he"llo = world


  1. OlegZH
    04.07.2022 23:17
    -5

    Добавь в Python немного C#... Что может быть лучше чашечки тёмного кофе со сливками и сахаром ранним утром после тарелки овсяной каши?!