10 предпочтительных методов рефакторинга кода на Python

Автор оригинала: Yong Cui
  • Перевод

Сделайте свой Python код читабельнее и производительнее


image


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


1. Включение (прим. пер.: абстракции) списков, словарей и множеств


Есть три типичных изменяемых (прим. пер.: мутабельных) контейнерных типа данных в Python: списки, словари и множества. Если у нас есть итерируемый объект, то мы можем воспользоваться циклом for, чтобы перебрать этот объект для создания на его основе нового списка. Однако характерный для Python способ – использовать включение списка, синтаксис которого выглядит следующим образом: [выражение for x in итерируемый_объект if условие]. Следует заметить, что часть с вычисляемым условием необязательна. Давайте посмотрим на следующий код:


>>> # Начнем с создания списка чисел
>>> numbers = list(range(5))
>>> print(numbers)
[0, 1, 2, 3, 4]
>>> 
>>> # Вместо этого:
>>> squares0 = []
>>> for number in numbers:
...     if number%2 == 0:
...         squares0.append(number*number)
... 
>>> print(squares0)
[0, 4, 16]
>>> 
>>> # Характерный для Python способ:
>>> squares1 = [x*x for x in numbers if x%2 == 0]
>>> print(squares1)
[0, 4, 16]

Включение списка


Помимо включения списка мы также можем использовать включения для словарей и множеств, что соответствует терминам «включение словаря» и «включение множества». Для включения словаря используется синтаксис: {выражение_ключа: выражение_значения for элемент in итерируемый_объект}, в то время как синтаксис включения множества – {выражение for элемент in итерируемый_объект}. Данный код демонстрирует их использование:


>>> # Включение словаря
>>> {x: x*x for x in range(5)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
>>> 
>>> # Включение множества
>>> {x*x for x in range(5)}
{0, 1, 4, 9, 16}

Включения словаря и множества


2. F-строки


Строки – привычный примитивный тип данных, который мы используем почти во всех проектах. В большинстве случаев для отображения строковых данных нам нужно сделать дополнительный шаг — определенным образом отформатировать их. Можно выполнить форматирование в стиле языка C, что подразумевает использование символа %, либо воспользоваться методом format, которым обладают строки в Python.


Однако в одном из недавних релизов Python был представлен новый метод форматирования строк. Он известен как «f-строки», что означает «форматируемые строковые литералы» – лаконичный и читабельный способ форматирования строк. Давайте посмотрим на сравнение этих разных подходов к форматированию


>>> # Определим коэффициент конвертации
>>> usd_to_eur = 0.89
>>> 
>>> # Вместо этого:
>>> print('1 USD = {rate:.2f} EUR'.format(rate=usd_to_eur))
1 USD = 0.89 EUR
>>> print('1 USD = %.2f EUR' % usd_to_eur)
1 USD = 0.89 EUR
>>> 
>>> # Характерный для Python способ:
>>> print(f'1 USD = {usd_to_eur:.2f} EUR')
1 USD = 0.89 EUR

F-строки


Естественно, вышеприведенный код показывает только основы использования f-строк, которые в сущности реализуют почти все стили форматирования, поддерживаемые форматированием в стиле C или методом str.format. Можете прочитать больше о f-строках в официальной документации или в моей недавней статье.


3. Множественное присваивание и распаковка кортежей


При работе с переменными определение одной переменной на одной строке – устоявшаяся практика. Однако при объявлении нескольких переменных мы можем сделать это на одной строке. Во избежание путаницы, пожалуйста, обратите внимание, что таким образом допускается объявлять переменные, которые связаны по смыслу. Если они используются для разных целей, то я бы не рекомендовал делать их множественное присваивание. Давайте посмотрим на рефакторинг:


>>> # Вместо этого:
>>> code = 404
>>> message = "Not Found"
>>> 
>>> # Характерный для Python способ:
>>> code, message = 404, "Not Found"

Множественное присваивание


За кулисами множественное присваивание предусматривает создание кортежа с правой стороны и его распаковку с левой стороны. Следующий код показывает, как распаковать кортеж. Как вы видите, это напоминает множественное присваивание, потому что используется один и тот же принцип.


>>> # Определяем и распаковываем кортеж
>>> http_response = (404, "Not Found")
>>> code, message = http_response
>>> print(f'code: {code}; message: {message}')
code: 404; message: Not Found

Распаковка кортежа


4. Распаковка с ловушками


В предыдущем разделе мы рассмотрели, как распаковать кортеж, используя наиболее базовый формат – количество переменных соответствовало количеству элементов кортежа. Однако, когда в кортеже есть несколько элементов, иногда может понадобиться распаковать, используя метод ловушек (прим. пер.: англ. catch-all method – под ловушкой подразумеваю переменную с префиксом-звездочкой, принимающую/захватывающую некоторое количество элементов контейнера при его распаковке, т. е. это не общепринятый термин, т. к. общепринятого не нашел, но это лучшее, что пришло на ум). А именно – все элементы, которые явно не обозначены переменными, будут захвачены переменной с префиксом-звездочкой. Для получения того же результата, нехарактерный для Python подход обычно подразумевает использование срезов, что чревато ошибками, если мы зададим некорректные индексы.


>>> # Определяем кортеж для распаковки
>>> numbers = tuple(range(8))
>>> numbers
(0, 1, 2, 3, 4, 5, 6, 7)
>>> 
>>> # Вместо этого:
>>> first_number0 = numbers[0]
>>> middle_numbers0 = numbers[1:-1]
>>> last_number0 = numbers[-1]
>>> 
>>> # Характерный для Python способ:
>>> first_number1, *middle_numbers1, last_number1 = numbers
>>> 
>>> # Проверяем результаты
>>> print(first_number0 == first_number1)
True
>>> print(middle_numbers0 == middle_numbers1)
False
>>> print(last_number0 == last_number1)
True

Распаковка с ловушками


Как вы могли заметить, значения middle_numbers0 и middle_numbers1 не равны. Это потому, что распаковка с ловушками (с использованием звездочки) по умолчанию генерирует объект списка. Таким образом, чтобы у распакованных данных на выходе был тот же тип данных, мы можем использовать конструктор кортежа, как показано ниже:


>>> # Конвертируем распакованный список в кортеж
>>> print(middle_numbers0 == tuple(middle_numbers1))
True

Создание кортежа из переменной-ловушки


5. Выражение присваивания


В выражении присваивания, более известном как «моржовое выражение», используется «оператор-морж» :=, который напоминает моржа с парой глаз и бивнями, не правда ли? Как понятно из его имени, выражение присваивания позволяет присваивать значение переменной и, в то же время, оно может быть использовано в качестве выражения, например таком, как условный оператор. Определение звучит запутанно, но давайте посмотрим на его использование в следующем фрагменте кода:


>>> # Определим некоторые вспомогательные функции
>>> def get_account(social_security_number):
...     pass
... 
>>> def withdraw_money(account_number):
...     pass
... 
>>> def found_no_account():
...     pass
... 
>>> # Вместо этого:
>>> account_number = get_account("123-45-6789")
>>> if account_number:
...     withdraw_money(account_number)
... else:
...     found_no_account()
... 
>>> # Характерный для Python способ:
>>> if account_number := get_account("123-45-6789"):
...     withdraw_money(account_number)
... else:
...     found_no_account()

Выражение присваивания


Как показано выше, когда мы не используем выражение присваивания, нам приходится сначала получить номер счета, а потом использовать его для операции снятия денег (прим. пер.: функция withdraw_money), что создает некоторое дублирование кода. В противоположность этому, мы можем избавиться от одной строки кода с помощью выражения присваивания, которое вызывает функцию и присваивает возвращенное ею значение новой переменной, которая в то же время вычисляется.


Некоторые бы стали утверждать, что экономия одной строки кода не так важна, но это делает более явным наше намерение – переменная account_number актуальна только в области видимости условного оператора. Если у вас есть опыт программирования на Swift, то использование выражения присваивания в условном операторе очень похоже на технику опционального связывания (прим. пер.: или опциональный биндинг), как показано ниже. По сути временная переменная accountNumber, если она содержит значение, используется только в идущей далее области видимости. Таким образом, вам следует познакомиться с выражением присваивания, и через некоторое время вы будете считать такой код более читабельным.


if let accountNumber = getAccount("123-45-6789") {
    withdrawMoney(accountNumber)
} else {
    foundNoAccount()
}

Опциональное связывание


6. Итерирование с использованием функции enumerate


Почти в каждом проекте нам неминуемо приходится заставлять нашу программу повторять определенные операции для всех элементов в списке, кортеже или каком-либо другом контейнерном типе данных. Добиться выполнения этих повторяющихся операций можно с помощью цикла for. Как правило, мы можем использовать его базовую форму: for элемент in итерируемый_объект. Однако, при таком итерировании, если нам нужен подсчет текущей итерации цикла, лучше воспользоваться функцией enumerate, которая может создать для нас счетчик. Более того, мы можем установить первоначальное значение счетчика.


>>> # Создаем список студентов для итерирования
>>> students = ['John', 'Jack', 'Jennifer', 'June']
>>> 
>>> # Вместо этого:
>>> for i in range(len(students)):
...     student = students[i]
...     print(f"# {i+1}: {student}")
... 
# 1: John
# 2: Jack
# 3: Jennifer
# 4: June
>>>
>>> # Характерный для Python способ:
>>> for i, student in enumerate(students, 1):
...     print(f"# {i}: {student}")
... 
# 1: John
# 2: Jack
# 3: Jennifer
# 4: June

Итерирование с помощью enumerate


7. Связывание итерируемых объектов с помощью zip/zip_longest


Предположим, что мы у нас есть два итерируемых объекта, и мы хотим объединить эти два объекта для каждой соответствующей пары элементов. Обычно, можно воспользоваться методом обращения по индексу, извлекая элементы из каждого итерируемого объекта таким образом, чтобы иметь возможность объединять их для формирования словаря. Однако, в Python есть встроенная функция zip, которая делает именно то, чего мы хотим добиться. В сущности, функция zip принимает любое количество итерируемых объектов и создает итерируемый объект той же длины, что и у наиболее короткого итерируемого объекта. Давайте посмотрим на такой пример:


>>> # Создаем два списка объектов для использования функции zip
>>> students = ['John', 'Mike', 'Sam', 'David', 'Danny']
>>> grades = [95, 90, 98, 97]
>>> 
>>> # Вместо этого:
>>> grade_tracking0 = {}
>>> for i in range(min(len(students), len(grades))):
...     grade_tracking0[students[i]] = grades[i]
... 
>>> print(grade_tracking0)
{'John': 95, 'Mike': 90, 'Sam': 98, 'David': 97}
>>> 
>>> # Предпочтительный способ:
>>> grade_tracking1 = dict(zip(students, grades))
>>> print(grade_tracking1)
{'John': 95, 'Mike': 90, 'Sam': 98, 'David': 97}
>>>
>>> from itertools import zip_longest
>>> grade_tracking2 = dict(zip_longest(students, grades))
>>> print(grade_tracking2)
{'John': 95, 'Mike': 90, 'Sam': 98, 'David': 97, 'Danny': None}

Использование zip с итерируемыми объектами


Функция zip создает итератор – объект класса zip, каждый элемент которого представляет собой кортеж, состоящий из элементов итерируемых объектов, переданных в функцию. Еще стоит отметить, что по умолчанию функция zip остановится, когда будет в каком-либо итерируемом объекте закончатся элементы. В противоположность этому, функция zip_longest будет ориентироваться на наиболее длинный итерируемый объект.


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


>>> for student, grade in zip(students, grades):
...     print(f"{student}: {grade}")
... 
John: 95
Mike: 90
Sam: 98
David: 97

Итерирование с использованием функции zip


8. Конкатенация (прим. пер.: сцепление) итерируемых объектов


В примере выше мы использовали функцию zip для поэлементного объединения итерируемых объектов. Что делать, если возникла необходимость в конкатенации итерируемых объектов? Предположим, что нам нужно пройтись по двум итерируемым объектам одной и той же категории для выполнения одной и той же операции. Мы можем получить такую возможность, используя функцию chain. Давайте посмотрим на пример ее использования:


>>> # Определяем данные и вспомогательную функцию
>>> from itertools import chain
>>> group0 = ['Jack', 'John', 'David']
>>> group1 = ['Ann', 'Bill', 'Cathy']
>>> 
>>> def present_project(student):
...     pass
... 
>>> # Вместо этого:
>>> for group in [group0, group1]:
...     for student in group:
...         present_project(student)
... 
>>> for student in group0 + group1:
...     present_project(student)
... 
>>> # Характерный для Python способ:
>>> for student in chain(group0, group1):
...     present_project(student)

Сцепление итерируемых объектов


Как показано выше, нехарактерный для Python подход подразумевает необходимость создания дополнительных списков — не самый эффективный с точки зрения памяти путь. В отличие от этого, функция chain создает итерируемый объект из тех итерируемых, которые были предварительно определены. Более того, функция chain гибкая и может принимать итерируемые объекты любого типа: словари, множества, списки, объекты классов zip и map (посредством использования функции map), а также многие другие типы итерируемых объектов Python.


9. Тернарное выражение


Что если нам нужно присвоить что-либо переменной таким образом, чтобы на основе условия были присвоены разные значения? В этом случае мы можем использовать условный оператор, чтобы вычислить условие и определить, какое значение должно быть использовано для присваивания. Обычно это подразумевает несколько строк кода. Однако, чтобы справиться с этой работой, мы можем использовать тернарное выражение, которое занимает всего лишь одну строку кода и обладает следующим основным синтаксисом: var = значение_в_случае_true if условие else значение_в_случае_false. Давайте рассмотрим соответствующие примеры.


>>> # Определяем вспомогательную функцию
>>> from random import randint
>>> def got_even_number():
...     return randint(1, 10) % 2 == 0
... 
>>> # Вместо этого: 
>>> if got_even_number():
...     state = "Even"
... else:
...     state = "Odd"
... 
>>> state = "Odd"
>>> if got_even_number():
...     state = "Even"
... 
>>> # Характерный для Python способ:
>>> state = "Even" if got_even_number() else "Odd"

Тернарное выражение


10. Использование генераторов


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


Рассмотрим следующий элементарный пример. Предположим, что нам нужно обработать тонны информации в файле. Теоретически, мы можем прочитать весь файл в список и обработать каждую строку данных в списке. Однако, вполне возможно, что ваш компьютер израсходует всю память, если файл невероятно большой. Вместо этого есть решение, которое лучше и более предпочтительно – создать генератор из файла, который предоставляет только одну строку данных при каждом запросе.


>>> # Определяем вспомогательную функцию
>>> def process_data(row_data):
...     pass
... 
>>> # Вместо этого:
>>> with open('large_file') as file:
...     read_data = file.read().split("\n")
...     for row_data in read_data:
...         process_data(row_data)
...
>>> # Характерный для Python способ:
>>> with open('large_file') as file:
...     for row_data in file:
...         process_data(row_data)

Генераторы


Заключение


Есть много еще более предпочтительных, естественных для Python способов осуществления разного рода вещей в Python, и темы, затронутые в этой статье – лишь неполный список приемов, которые я счел полезными в своих повседневных проектах. Надеюсь, они помогут писать код на Python и вам. Заключительный совет – вы должны целенаправленно делать рефакторинг своего кода, используя эти и другие характерные для Python приемы (прим. пер.: идиомы), которые обычно еще и более эффективные с точки зрения производительности. С помощью единообразного рефакторинга своего кода вы будете постепенно улучшать свои навыки программирования на Python.


Спасибо за чтение этого материала, и удачного программирования.

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 28

    0
    Спасибо огромное за статью, много интересного узнал, хоть и давно пишу на Python.

    Вопрос по примеру из использования генераторов.
    А если у нас большой файл, но нам надо сделать не
    file.read().split("\n")

    а, например,
    file.read().split("<...>")


    то есть другой, отличный от \n разделитель?
    В данный момент использую решение 10-летней давности с offset и seek(), которое работает но не столь элегентно… Может есть решение?
      0

      Написал такое:


      def custom_splitter(file, delimiter, remove_delimiters):
          counter = 0
          segment = ''
          del_length = len(delimiter)
          while char := file.read(1):
              segment += char
              counter += 1
              if counter % del_length == 0 and segment.endswith(delimiter):
                  yield segment[:-del_length] if remove_delimiters else segment
                  segment = ''
                  counter = 0
      
      with open('text.txt', encoding='utf-8') as file:
          for segment in custom_splitter(file, '<...>', True):
              print('-'*20)
              print(segment)

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

        0
        Ну так и есть :)
        Правда я читал блоками по N байт, каждый блок сплитил по разделителю, а остаток (который возможно неполный) переносил в следующую итерацию…
          0

          Тоже вариант, который, вероятно, лучше моего.
          Написал тот, о котором упоминал:


          def custom_splitter(file, delimiter, remove_delimiters):
              end = ''
              segment = []
              while char := file.read(1):
                  segment += [char]
                  end += char
                  if len(end) >= len(delimiter):
                      if end == delimiter:
                          yield ''.join(segment[:-len(delimiter)]) \
                              if remove_delimiters else ''.join(segment)
                          segment.clear()
                      end = end[1:]
              yield ''.join(segment)
      +1
      Так-то, конечно, полезная для новичков подборка.
      Но всё-таки это ни разу не методы рефакторинга, тут просто синтаксический сахар и несколько полезностей из стандартной библиотеки.
        0
        1. Включение (прим. пер.: абстракции) списков, словарей и множеств
        Обычно на русский язык эти сomprehensions переводят как генераторы. То есть генератор списка, генератор словаря и генератор множества.
        >>> print(f'1 USD = {usd_to_eur:.2f} EUR')
        Вот то что идет после двоеточия — это задание особенностей форматирования. Их несколько существует. Вот тут собрана таблица с примерами: mkaz.blog/code/python-string-format-cookbook
          0

          Тоже видел подобный перевод. Но, по-моему, он вносит некоторую путаницу – в Python этот термин уже используется для другой штуки, о которой, кстати, говорится в 10 пункте этой статьи. Т. е., было бы так:


          1. Генераторы
            ...
          2. Использование генераторов.
            0
            1. Генераторы
            Генераторы коллекций.
            говорится в 10 пункте этой статьи.
            ИМХО, то что в десятом пункте вообще не корректно называть генератором, я бы назвал это итератором (хотя это вопрос к автору а не переводчику).
            Генератор генерирует новую коллекцию, а итератор идет по существующей. В десятом пункте идет построчный обход существующих в файле строк, то есть итерация.
              0

              Похоже, вы правы. В десятом пункте действительно выполняется итерация по итератору строк файла через вызов метода __next__, если я корректно выразился.
              Наткнулся на такую, вроде бы неплохую, статью https://opensource.com/article/18/3/loop-better-deeper-look-iteration-python на эту тему.

                0

                Да, генераторы коллекций – путаницы действительно меньше.
                С другой стороны, теоретически может быть путаница с вещами наподобие этой:


                def what_am_i():
                    lst = []
                    for i in range(100):
                        lst.append(i)
                        yield lst[:]
                  +1
                  Это функция-генератор.
                  А есть еще выражения-генераторы (generator expressions). Основное их отличие от генераторов коллекций в том, что они выдают элемент по-одному, не загружая в память сразу всю коллекцию.
                  Я такую классификацию в своей статье использовал:
                  image
              0

              Да, вроде неплохая статья. Как-то читал в документации про форматирование – тяжело все запомнить.

                0

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

              0
              Лично я в основном избегаю генераторов списков. Они дают сравительно небольшое увеличение производительнсти, зато здорово снижают читабельость кода.
                +2
                Когда накапливается опыт их применения и чтения, то они наоборот в большинстве не слишком навороченных случаев увеличивают читабельность кода из-за компактности и однозначности записи.
                squared_evens = [n ** 2 for n in numbers if n % 2 == 0]
                Или
                squared_evens = []
                for n in numbers:
                    if n % 2 == 0:
                        squared_evens.append(n ** 2)
                  0

                  Я бы не сказал, что "здорово снижают". Со временем, если чаще ими пользоваться, то они воспринимаются лучше, чем поначалу. В них же еще можно использовать перенос строки, например, отделять с помощью него ту часть, которая до for.


                  squared_evens = [n**2
                                   for n in range(10)
                                   if n % 2 == 0]
                    0
                    Ну я бы сказал, в простейших случаях ничего, но чуть посложнее и нужно разбираться, что же там происходит.
                    Ваш пример ничего, но получается практически тот же for.
                    С другой стороны, я люблю вставлять для отладки печать или запись в лог, опять-таки это делать удобнее в for.
                    Вообще, я стараюсь писать так, чтобы в одной строке был один «логический кирпичик».
                      +1

                      Он визуально стал похож на for, но вряд ли потерял преимущество в производительности, которое вы упомянули. Насчет отладки согласен.

                    0

                    Конечно, если использовать тернарные выражения и более одного цикла / вложенные включения – тогда действительно прощай читабельность.

                      0
                      Еще можно путаницы добавить, если фильтрацию и ветвление одновременно использовать:
                      list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
                      list_b = [x**3 if x < 0 else x**2 for x in list_a if x % 2 == 0]
                      # вначале фильтр пропускает в выражение только четные значения
                      # после этого ветвление в выражении для отрицательных возводит в куб, а для остальных в квадрат
                      print(list_b)   # [-8, 0, 4, 16]
                        0

                        Как раз подобное имел в виду.

                          0

                          тривиальный случай, поняла с первого раза. там всё же английским по белому написано

                    +2
                    1. Множественное присваивание и распаковка кортежей
                      >> # Вместо этого:
                      >> code = 404
                      >> message = "Not Found"
                      >> 
                      >> # Характерный для Python способ:
                      >> code, message = 404, "Not Found"

                    Отличный пример, но читаемость ни разу не выросла. На code review я за такое бью по рукам.


                    1. Тернарное выражение
                      >> # Вместо этого: 
                      >> if got_even_number():
                      ...     state = "Even"
                      ... else:
                      ...     state = "Odd"
                      ... 
                      >> # Характерный для Python способ:
                      >> state = "Even" if got_even_number() else "Odd"

                    Тернарный оператор — очень скользкая дорожка. Чаще я сталкиваюсь с таким:


                    state = "Long description in case of `got_even_number` returns 1" \
                        if got_even_number() else \
                        "Another long description used when `got_even_number` returns 0"

                    В данном примере обычный if предпочтительнее. А ещё лучше обойтись вообще без if:


                    states = (
                        "Another long description used when `got_even_number` returns 0",
                        "Long description in case of `got_even_number` returns 1",
                    )
                    number = got_even_number()
                    state = states[number]
                      +2

                      Пример без if — умно. Для примера в статье, вероятно, лучше подойдет словарь?

                        0
                        Очень субъективно, и распаковка и тернарный оператор — отличные инструменты, повышающие читаемость кода, если их применять к месту. В примерах выше я бы предпочел распаковку.
                        0

                        Форматирование через f-строки сложно назвать улучшением. Они бывают удобными, но имеют ряд моментов:


                        1. Если забыть f перед строкой — получаем нерабочую строку, которую порою трудно отследить.
                        2. Возможность вставлять различные конструкции внутрь фигурных скобок ухудшает читабельность всей строки. И это плохо, поскольку в рамках проекта желательно все строки строить единым образом, а сложить все рядышком в метод format уже не получится.

                        Конечно, все это отчасти вкусовщина, но я не вижу потенциальной выгоды f-строк перед format'ом.

                          0

                          Отчасти, отследить строку с забытой перед ней f помогает подсветка фигурных скобок, по крайней мере в Visual Studio Code с расширением Python такое наблюдаю. Конечно, это не так заметно, по сравнению с сообщениями линтера.
                          В одной статье читал, что str.format якобы плох в том случае, когда в нем используются именованные аргументы, а именно в этой: https://realpython.com/python-f-strings/

                          0

                          очевидные вещи — такие очевидные

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое