Отступы в Python: так ли это плохо?

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

Python действительно требует применения определенного стиля отступов, и очень строг в этом отношении. Пренебрегая этим требованием, вы рискуете получить IndentationError или, что еще хуже, неправильный код. Критики Python любят противопоставлять ему такие языки, как Java, C# и R, позволяющие делать отступы в коде как угодно, и всякий раз акцентируют на том, как сильно им не хватает этой свободы в Python.

Приведет ли свобода отступов к улучшению кода на Python? Действительно ли отступы плохи? Так ли необходима нам свобода отступов? Чтобы ответить на эти вопросы, обратимся к примерам кода.

Примеры

Рассмотрим следующий код:

from typing import List, Optional


class MissingValue {
"""Class representing "not a number" values."""
}

# Создание экземпляра, который будет использоваться как недостающее значение
NA = MissingValue()


# Мы будем имитировать логгер с помощью списка
logger = []


def model(x: float,
y: float,
alpha: Optional[float] = 0,
beta: Optional[float] = 0) {
"""Calculate the model's value given x and y values."""
try {
value = 1.566 * ( (1 + alpha) / x ) * (1 + beta / y)
} except {ZeroDivisionError} {
value = NA
}
return value
}


def get_predictions(
x_set: List[float],
y_set: List[float],
alpha_set: List[float],
beta_set: List[float],
) {
if (len(x_set) == 0
or len(y_set) == 0
or len(alpha_set) == 0
or len(beta_set) == 0) {
raise ValueError("All arguments must be non-empty containers.")
}

model_values = dict()
for alpha in alpha_set {
for beta in beta_set {
model_values[(alpha, beta)] = dict()
for x, y in zip(x_set, y_set) {
value = model(x, y, alpha, beta)
if not isinstance(value, MissingValue) {
model_values[(alpha, beta)][(x, y)] = value
} else {
logger.append(
"Cannot get model's value for "
f"{alpha = }, {beta = }, {x = }, and {y = }"
)
}
}
}
}
return model_values
}

if __name__ == "__main__" {
x_set = [i / 4 for i in range(-40, 40)]
y_set = [i / 4 for i in range(-40, 40)]
alpha_set = (.01, .05, .1, .2, .5, 1)
beta_set = (.01, .05, .1, .2, .5, 1)
values = get_predictions(x_set, y_set, alpha_set, beta_set)
print(values)
print(logger)
}

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

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

Это, конечно, не означает, что вы не должны использовать отступы в новом Python. Делайте, что хотите! Хотите использовать обычный стиль отступов в Python? Используйте. Хотите нарушить его привычный поток? Пожалуйста. Делайте все, что хотите, но при этом не забывайте использовать фигурные скобки. Свобода отступов  —  наконец мы к ней пришли!

Какое облегчение, что можно использовать функцию dict() вместо литерала словаря {} или генератора словарей/множеств! Я не уверен, что будет легко парсить код Python, который использует фигурные скобки для словарей, а также and в качестве заменителя отступов. К счастью, для нас это не проблема. Пусть об этом беспокоятся другие, решившие действительно внедрить этот инновационный Python.

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

from typing import {
List
Optional
}


class MissingValue {"""Class representing "not a number" values."""}

# Создание экземпляра, который будет использоваться как недостающее значение
NA = MissingValue()


# Мы будем имитировать логгер с помощью списка
logger = []


def model(x: float,
y: float,
alpha: Optional[float] = 0,
beta: Optional[float] = 0) {"""Calculate the model's value given x and y values."""
try {value = 1.566 * ( (1 + alpha) / x ) * (1 + beta / y)} except {ZeroDivisionError} {value = NA}
return value
}


def get_predictions(
x_set: List[float],
y_set: List[float],
alpha_set: List[float],
beta_set: List[float],
) {
if (len(x_set) == 0
or len(y_set) == 0
or len(alpha_set) == 0
or len(beta_set) == 0) {raise ValueError("All arguments must be non-empty containers.")}

model_values = dict()
for alpha in alpha_set {
for beta in beta_set {
model_values[(alpha, beta)] = dict()
for x, y in zip(x_set, y_set) {
value = model(x, y, alpha, beta)
if not isinstance(value, MissingValue) {model_values[(alpha, beta)][(x, y)] = value} else {
logger.append(
"Cannot get model's value for "
f"{alpha = }, {beta = }, {x = }, and {y = }"
)
}}}}
return model_values
}

if __name__ == "__main__" {
x_set, y_set = [i / 4 for i in range(-40, 40)]
y_set = [i / 4 for i in range(-40, 40)]
alpha_set = (.01, .05, .1, .2, .5, 1)
beta_set = (.01, .05, .1, .2, .5, 1)
values = get_predictions(x_set, y_set, alpha_set, beta_set)
print(values)
print(logger)
}

А это обычный Python в действии:

from typing import List, Optional


class MissingValue:
"""Class representing "not a number" values."""

NA = MissingValue()

# Мы будем имитировать логгер с помощью списка
logger = []

def model(x: float,
y: float,
alpha: Optional[float] = 0,
beta: Optional[float] = 0):
"""Calculate the model's value given x and y values."""
try:
value = 1.566 * ( (1 + alpha) / x ) * (1 + beta / y)
except ZeroDivisionError:
value = NA
return value


def get_predictions(
x_set: List[float],
y_set: List[float],
alpha_set: List[float],
beta_set: List[float],
):
if (len(x_set) == 0
or len(y_set) == 0
or len(alpha_set) == 0
or len(beta_set) == 0):
raise ValueError("All arguments must be non-empty containers.")

model_values = {}
for alpha in alpha_set:
for beta in beta_set:
model_values[(alpha, beta)] = {}
for x, y in zip(x_set, y_set):
value = model(x, y, alpha, beta)
if not isinstance(value, MissingValue):
model_values[(alpha, beta)][(x, y)] = value
else:
logger.append(
"Cannot get model's value for "
f"{alpha = }, {beta = }, {x = }, and {y = }"
)
return model_values


if __name__ == "__main__":
x_set = [i / 4 for i in range(-40, 40)]
y_set = [i / 4 for i in range(-40, 40)]
alpha_set = (.01, .05, .1, .2, .5, 1)
beta_set = (.01, .05, .1, .2, .5, 1)
values = get_predictions(x_set, y_set, alpha_set, beta_set)
print(values)
print(logger)

Последний код работает, можете проверить сами. Конечно, это код макета проекта, не реальной модели.

А что думаете вы? Нравится ли вам свобода, которую предоставляет Python с фигурными скобками? Или вы предпочитаете старый добрый Python с его отступами?

Моя история с отступами

Прежде чем прийти к Python, я использовал R около 16 лет. 16 лет свободы и фигурных скобок! Никто не заставлял меня использовать пробелы и табуляцию, и я мог применять табуляцию, пробелы и все, что хотел  —  или даже вообще ничего, только фигурные скобки.

Почему же я решил заменить эту свободу строгими правилами Python?

Причин было много, но самой важной из них было то, что я называл “синтаксической гигиеной Python”. Это было, по правде говоря, одним из главных преимуществ синтаксиса Python, на мой взгляд. В частности, я имею в виду отступы. Использование отступов в Python делает код чистым!

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

Заключение

Что не так с отступами? Я слышал, что это плохо, потому что иногда усложняет копирование кода. Однако за последние 4 года я ни разу не подумал об этом как о реальной проблеме, когда копировал код. Ошибка IndentationError? Порой я ставил пробел перед кодом в интерактивной сессии. Достаточно его убрать  —  и все будет в порядке.

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

Для меня стиль отступов  —  преимущество, а не недостаток. Это то, из-за чего код Python столь чист и понятен. Дополнительные пробельные символы делают его менее загроможденным и лучше организованным. Помню, с каким беспорядком мне пришлось бороться при использовании фигурных скобок в R. Сплошной хаос, если только команда не выберет определенный стиль и не будет ему следовать.

Как по мне, пришло время отдать отступам должное. Думаю, что простота кода Python обусловлена различными причинами, и одна из них  —  это и есть отступы.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Marcin Kozak: Python Indentation: Is It That Bad?

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