Форматируем код при помощи black

Опубликовано: 26 мая 2020, Вт, автор: Андрей Семакин Обновлено: 26 мая 2020, Вт 7 минут

Почему полезно форматировать код

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

Почему же консистентный стиль оформления кода так важен, что этому посвящен целый PEP? Самая главная мысль — читаемость важна. Вот какие ещё мысли у меня возникают по поводу стиля оформления кода.

  • Код быстро меняется. Реалии разработки ПО сегодня очень отличаются от того, что было 50 или 30 лет назад. Тогда код мог быть написан единожды программистом-одиночкой, и работать десятилетиями без изменений. Сегодня код чаще разрабатывается командами программистов, и меняться он должен постоянно, реагируя на изменчивые обстоятельства внешней среды. К консистентно оформленному коду намного проще вернуться в будущем, понять его, найти ошибку и исправить, или допилить новую фичу.
  • Код пишется один раз, а читается множество раз. Сэкономленная сегодня минута на консистентном оформлении кода может вылиться в часы чтения и попыток понять этот же код завтра (или через полгода). Когда код оформлен одинаково, мозгу не нужно отвлекаться на незначащие детали оформления. Так намного проще вникнуть в суть написанного. В командной разработке просто необходим общий стиль, которого будут придерживаться все.
  • Код пишется для людей, а не для компьютеров. Машинам абсолютно индифферентно, сколько вы там ставите пробелов, пустых строк, какую длину строки используете или где вы ставите запятые. Если код синтаксически верен, то компьютер отлично вас поймёт. Языки высокого уровня (такие как Python) были специально разработаны для того, чтобы как можно дальше уйти от написания непонятного людям машинного кода. Мы пишем код для людей. Но людям очень важны такие незначащие детали, как отступы, запятые и скобки. С их помощью можно сделать код как понятным с первого взгляда, так и сложным и запутанным.
  • Время программиста стоит дорого. Писать хорошо оформленный код просто дешевле на долгих дистанциях. Допустим, что оформление кода поможет ускорить его понимание пусть даже всего на 1 минуту. Умножьте на количество человек, которые будут читать этот код, и на (потенциально бесконечное) количество раз, которое они будут к нему возвращаться, и на стоимость работы одного программиста за минуту — получаете кучу сэкономленных деняк.
  • Консистентный код сокращает количество дискуссий на ревью, потому что коллегам уже не приходится напоминать про скобки и запятые.
  • Консистентный код порождает меньшие диффы, так что его элементарно проще ревьювить.

Следование PEP8 можно автоматически контролировать при помощи таких инструментов, как flake8 или pylint, но тогда форматировать код придётся вручную. Как мы уже выяснили, время разработчика стоит дорого. Можно ли как-то автоматизировать этот процесс?

Кроме того, PEP8 описывает лишь основные правила оформления кода, но оставляет свободу интерпретации во множестве краевых случаев, из-за чего может появиться неконсистентность оформления. Есть ли какие-нибудь более строгие конвенции, чем PEP8?

black — бескомпромиссный форматтер кода

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

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

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

# так?
def func("arg1",
         "arg2",
         kwarg="value")

# или так?
def func(
    "arg1",
    "arg2",
    kwarg="value"
)

У меня был опыт попытки выработать в команде конвенцию, и это не так просто. Не потому что люди плохие или упрямые, просто у каждого своё понимание красоты и удобства. Наверняка, начнётся борьба вкусов и мнений. black избавляет от всех этих обсуждений — есть готовый стиль оформления кода, где все решения уже приняты за вас.

Да, иногда black выдаёт не самый красивый код, но, если подумать, красота кода не так важна, как консистентность, последовательность, одинаковость. black осознанно приносит красоту в жертву консистентности.

Скажу по собственному опыту, что к отформатированному black коду привыкаешь очень быстро, и буквально через несколько дней просто перестаешь замечать форматирование вообще. Просто читаешь код.

Установка

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

black устанавливается из PyPI. Давайте выясним, какая на данный момент последняя доступная версия при помощи следующего трюка (или можно просто её посмотреть на странице проекта на PyPI):

$ pip install black==
Collecting black==
  ERROR: Could not find a version that satisfies the requirement black== (from versions: 18.3a0, 18.3a1, 18.3a2, 18.3a3, 18.3a4, 18.4a0, 18.4a1, 18.4a2, 18.4a3, 18.4a4, 18.5b0, 18.5b1, 18.6b0, 18.6b1, 18.6b2, 18.6b3, 18.6b4, 18.9b0, 19.3b0, 19.10b0)
ERROR: No matching distribution found for black==

Команда завершится ошибкой, но выведет список доступных версий. Найдем последнюю доступную версию и запомним её.

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

$ pip install black==19.10b0

А если вы используете pipenv или poetry, то вот так:

$ pipenv install --dev black==19.10b0
$ poetry add --dev black==19.10b0

Обратите внимание, что при установке black через pipenv обязательно нужно указывать конкретную версию. Я описал, что произойдёт, если этого не сделать, а взамен разрешить pipenv устанавливать пре-релизные версии в посте про pipenv.

Использование

black имеет очень интуитивный интерфейс командной строки.

Вот так можно отформатировать все файлы в текущей директории (и рекурсивно в поддиректориях):

$ black .
reformatted /home/br0ke/git/pipenv/pipenv/cli/__init__.py
reformatted /home/br0ke/git/pipenv/pipenv/__init__.py
...
All done! ✨ 🍰 ✨
50 files reformatted, 11 files left unchanged.

И это практически единственная команда, которую вам нужно запомнить.

А вот так можно отформатировать один конкретный файл:

$ black setup.py 
reformatted setup.py
All done! ✨ 🍰 ✨
1 file reformatted.

Интеграция с редактором/IDE

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

Использование в CI

А ещё нужно настроить запуск black в сервисе для непрерывной интеграции (CI), например, GitHub Actions, GitLab CI или Travis CI. Так black сможет блокировать пулл-реквесты (или мердж-реквесты), в которых содержится неотформатированный код.

$ black --check .

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

Конфигурация

Кое-какую минимальную возможность настройки black все-таки предоставляет. Стоит отметить, что в большинстве случаев этого делать не придётся, потому что у black достаточно разумные настройки по умолчанию.

Вот так можно настроить максимальную длину строки и файлы, которые форматировать не нужно. В pyproject.toml в корне проекта добавьте:

[tool.black]
line-length = 88
include = '\.pyi?$'
exclude = '''
(
  /(
      \.eggs         # exclude a few common directories in the
    | \.git          # root of the project
    | \.hg
    | \.mypy_cache
    | \.tox
    | \.venv
    | _build
    | buck-out
    | build
    | dist
  )/
  | foo.py           # also separately exclude a file named foo.py in
                     # the root of the project
)
'''

Настройка flake8, чтобы он не противоречил black

У flake8 своё мнение по поводу того, как должен быть отформатирован код, которое не всегда совпадает с мнением black. Чтобы не возникало конфликтов, рекомендуется выключить некоторые проверки flake8, по примеру того, как это сделано в репозитории black.

Что меня бесит в стиле black

Есть некоторые моменты, с которыми я категорически несогласен. Думаю, рассказать про них тоже нужно.

Рассмотрим пример исходного кода:

print(f"Hello, {user_name}!",
      "What a wonderful day!",
      "I don't know you. Are you new here?" if user_name != "Andrey" else "Nice to meet you again!",
      "Today is",
      datetime.datetime.now().isoformat())

Я специально сделал побольше аргументов в функцию print(), чтобы вызов функции стал достаточно длинным, чтобы black разнёс его на несколько строк. Обратите внимание на тернарный оператор. Теперь отформатируем и посмотрим на результат:

print(
    f"Hello, {user_name}!",
    "What a wonderful day!",
    "I don't know you. Are you new here?"
    if user_name != "Andrey"
    else "Nice to meet you again!",
    "Today is",
    datetime.datetime.now().isoformat(),
)

Тернарный оператор затерялся среди других аргументов функции, его теперь очень трудно заметить. Чтобы исправить, давайте возьмём тернарный оператор в скобки и ещё раз отформатируем:

print(
    f"Hello, {user_name}!",
    "What a wonderful day!",
    (
        "I don't know you. Are you new here?"
        if user_name != "Andrey"
        else "Nice to meet you again!"
    ),
    "Today is",
    datetime.datetime.now().isoformat(),
)

Теперь намного понятнее, не правда ли? Такое может произойти не только с тернарными операторами, но и с длинными арифметическими выражениями, и с длинными строками, которые разбиты на несколько частей:

func(True,
     # Обратите внимание, что между следующими "строками" нет
     # запятой, так что фактически это один строковый литерал.
     # Хороший приём для объединения длинных строк внутри скобок.
     "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
     "Sed dolor massa, mollis a commodo in, molestie in risus.")

Эта функция имеет два аргумента — булевый и строковый. Отформатируем:

func(
    True,
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
    "Sed dolor massa, mollis a commodo in, molestie in risus.",
)

black опять не помог сделать код читаемее. Выглядит так, будто функция имеет три аргумента, хотя на самом деле их только два. Нужно хорошо присмотреться, чтобы заметить отсутствие запятой. Поставим скобки вручную и отформатируем ещё раз:

func(
    True,
    (
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
        "Sed dolor massa, mollis a commodo in, molestie in risus."
    ),
)

Стало в сто раз лучше. Теперь всё очевидно.

Надеюсь, что когда-нибудь эту шероховатость починят, а до тех пор я просто вручную ставлю скобки вокруг вот такого некрасивого кода. Такое случается не так уж часто. В целом, даже почти не больно.

Заключение

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

Хоть black и имеет свои недостатки, он всё равно явно окупает усилия, затраченные на ручную расстановку скобок, потому что случается это довольно редко. Как и любой инструмент, black дорабатывается, и будем верить, что все проблемы рано или поздно исправят.

black, наряду с flake8 и pytest, попал в мой набор незаменимых инструментов, которые я пытаюсь использовать во всех своих проектах. И вам рекомендую.

Если понравилась статья, то подпишитесь на уведомления о новых постах в блоге, чтобы ничего не пропустить!

Дополнительное чтение

тэги: python, pep8, black, formatter