[Перевод] Чистый код в Python
Всем привет!
Это перевод статьи Clean Code in Python, в которой Nik Tomazic рассказывает о чистом коде, его преимуществах, различных стандартах и принципах, но что самое главное‑ он дает общие рекомендации по написанию чистого кода. Прочитав данную статью в оригинале, я понял, что это то, что я хотел бы прочитать в самом начале своего пути разработки на Python. Именно это и вдохновило меня на создание первого перевода, а вместе с этим, и первой публикации на Хабре.
Содержание
Что такое чистый код?
Стандарты кода
Принципы написания кода
Форматирование кода
Чистый код на Python: руководство по написанию
Модульность и классы
Тестирование
Заключение
Что такое чистый код?
Чистый код– это набор правил и принципов, помогающих сделать наш код читаемым, обслуживаемым и масштабируемым. Это одна из самых важных тем, без которой невозможна разработка качественного ПО. Мы тратим больше времени на чтение когда, чем на его написание и это подчеркивает важность написание хорошего кода.
Писать код легко, но писать хороший и чистый код– сложно.
Код, который мы пишем, должен быть простым, понятным и не должен содержать большого количества дублирующегося кода.
Важность чистого кода
Разработка с использованием практик и принципов чистого кода дает множество преимуществ. Как правило, подобный код:
Легкий в понимании;
Эффективный;
Прост в обслуживании, расширении, отладке и рефакторинге; Кроме всего прочего, данный код не нуждается в большом количестве документации, так как он говорит сам за себя, а низкая когнитивная сложность не вызывает лишних вопросов.
Стандарты кода
Стандарты кода– это набор правил, руководств и лучших практик. Каждый язык программирования имеет свои стандарты разрабитки, которым стоит следовать при разработке чистого кода.
Обычно, они направлены на:
Организацию файлов;
Использование общеизвестных принципов и лучших практик;
Форматирование кода (отступы, объявления и определения);
Наименования
Комментарии
PEP 8 (Python Enhancement Proposal)
PEP 8– это руководство по стилю, описывающее стандарты разработки на Python. Это самое популярное руководство в сообществе Python. Ниже я приведу самые важные правила.
Соглашение о наименованиях:
Имена классов должны соответствовать CamelCase (
MyClass
);Имена переменных должны быть в стиле snake_case и состоять только из строчных букв (
my_name_variable
);Имена функции так же должны быть в стиле snake_case и состоять только из строчных букв (
some_function()
);Константы должны быть в стиле snake_case и состоять только из заглавных букв (
PI = 3.14159
);Модули должны иметь короткое название в стиле snake_case и состоять только из строчных букв (
pytest
);Единообразные кавычки, одинарные или двойные. Они обрабатываются одинаково (просто выберите один из доступных типов ковычек и следуйте ему);
Форматирование строк:
Для отступа используйте 4 пробела (табуляция допустима, но пробелы предпочтительнее);
Строка не должна содержать более 79 символов;
Строка не должна содержать более одного определения;
Определение функций и классов должны быть разделены между собой двумя пустыми строками;
Определение функций внутри класса должны быть разделены между собой одной пустой строкой;
Импорты должны быть на разных строках;
Пробелы:
Избегайте большого колличества пробелов в квадратных или фигурных скобках;
Строки не должны заканчиваться пробелом;
Всегда окружайте бинарные операторы одинарными пробелами;
Если используются операторы с разным приоритетом, отделите самые низкоприоритетныt операторы пробелами с двух сторон;
Не используйте пробелы вокруг
=
, при определении именованного аргумента;
Комментарии:
Комментарии не должны противоречить коду;
Комментарии должны быть предложениями (читабельны и понятны);
Комментарий должен содержать пробел после
#
и начинаться с заглавной буквы;Многострочные комментарии, используемые для описания функций (docstrings), должны иметь короткое однострочное название, которое раскроет смысл текста, идущего после него;
Если вам интересно, рекомендую ознакомиться с официальной документацией PEP 8.
Питоничный код
Питоничный код соответствует набору идиом, определенных сообществом Python. Это значит, что вам просто нужно следовать идиомам и парадигмам, что бы сделать ваш код чище, читаемее и производительнее.
Питоничный код включает:
Трюки при работе с переменными;
Манипуляции над листами (создание, разрезы);
Работа с функциями;
Явные действия в коде (явное ведь лучше не явного, скоро вы это узнаете)
Вообще, есть большая ращница между написанием Python кода и написанием «Pythonic» кода. Что бы получить питоничный код, вы не можете просто идиоматически перевести код, написанный на другом языке (таком как Java или C++) в Python код; Вам нужно «думать на Python» и знать его особенности, что бы получить питоничный код.
Давайте лучше рассмотрим пример. Нам нужно получить сумму первых 10 чисел.
Не питоничное решение выглядело бы примерно так:
n = 10
sum_all = 0
for i in range(1, n + 1):
sum_all = sum_all + i
print(sum_all) # 55`
А вот и «Pythonic» решение:
n = 10
sum_all = sum(range(1, n + 1))
print(sum_all)
Как вы видите, второй пример гораздо проще читать опытному разработчику, но он требует более глубокого понимания встроенных функций и синтаксиса Python. Самый простой способ писать код — это помнить о Дзене Python и знать стандартные библиотеки Python .
Дзен Питона
Дзен Питона — это набор из 19 «вспомогатильных принципов» для разработки на Python. Эти принципы были написаны в 1999 году разработчиком Тимом Питерсом и встроены в интерпретатор Python как пасхалка. Вы можете увидеть их, выполнив следующую команду в консоле python:
import this
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Красивое лучше, чем уродливое.
Явное лучше, чем неявное.
Простое лучше, чем сложное.
Сложное лучше, чем запутанное.
Плоское лучше, чем вложенное.
Разреженное лучше, чем плотное.
Читабельность имеет значение.
Особые случаи не настолько особенны, чтобы нарушать правила.
Хотя практичность побеждает чистоту.
Ошибки никогда не должны проходить молча.
Если только они явно не замалчиваются.
Перед лицом неоднозначности откажитесь от искушения угадать.
Должен быть один — и желательно только один — очевидный способ сделать это.
Хотя этот способ может быть неочевидным на первый взгляд, если вы не голландец.
Сейчас лучше, чем никогда.
Хотя никогда часто лучше, чем прямо сейчас.
Если реализацию трудно объяснить, это плохая идея.
Если реализацию легко объяснить, это может быть хорошей идеей.
Пространства имен — это одна из замечательных идей — давайте делать их больше!
Если вам стало интересно, но всё еще не совсем понятно, то рекомендую ознакомиться со статьей Тимура Гуева Zen of Python: история, реализация и пасхалки, в которой вы можете найти более подробный разбор принципов.
Принципы написания кода
Существует множество принципов, которых можно придерживаться для написания качественного кода. У каждого из них есть свои преимущества, недостатки и компромиссы. В этой статье рассмотрены четыре популярных принципа: DRY, KISS, SoC и SOLID.
DRY (Don«t Repeat Yourself)
Каждый фрагмент кода должен иметь единственное, однозначное и авторитетное представление в системе.
Это один из самых простых принципов разработки. Его единственное правило — код не должен дублироваться. Вместо дублирования строк найдите алгоритм, который использует итерацию. DRY-код легко поддерживать. Вы можете развить этот принцип еще дальше с помощью абстракции модели/данных.
Минусы принципа DRY в том, что вы можете получить слишком много абстракций, создание внешних зависимостей и сложный код. DRY также может вызвать осложнения, если вы попытаетесь изменить большую часть вашей кодовой базы. Вот почему вам следует избегать слишком раннего применения DRY к вашему коду. Всегда лучше иметь несколько повторяющихся разделов кода, чем неправильные абстракции.
KISS (Keep it Simple, Stupid)
Большинство систем работают лучше, если их не переусложнять.
Принцип KISS утверждает, что простота должна быть ключевой целью в проектировании, а ненужной сложности следует избегать.
SoC (Separation of Concerns)
SoC — это принцип разделения программы на отдельные модули, каждый из которых решает свою задачу.
Хороший пример SoC — это MVC (Model — View — Controller).
При выборе данного подхода будьте осторожны с избыточным делением приложения на модули. Новый модуль стоит создавать только тогда, когда это оправдано. Больше модулей — больше проблем.
SOLID
SOLID — это мнемоническая аббревиатура из пяти принципов проектирования, которые делают дизайн программного обеспечения более понятным, гибкими и удобными для обслуживания.
Принципы SOLID особенно полезны при написании кода по методологии ООП. Они касаются разделения классов, наследования, абстракции, интерфейсов и других аспектов проектирования.
Он состоит из следующих пяти концепций:
Single Responsibility Principle — «Класс должен иметь одну и только одну причину для изменения.»
Open–Closed Principle — «Сущности должны быть открыты для расширения, но закрыты для модификации.»
Liskov Substitution Principle — «Функции, использующие базовые классы, должны работать с объектами производных классов без изменений.»
Interface Segregation Principle — «Клиент не должен зависеть от интерфейсов, которые он не использует.»
Dependency Inversion Principle — «Зависимость должна быть от абстракций, а не от конкретных реализаций.»
Форматирование кода
Форматировщики кода обеспечивают стиль кодирования посредством автоматического форматирования и помогают достичь и поддерживать чистоту кода. Большинство из них позволяют вам создать файл конфигурации стиля, которым вы можете поделиться с коллегами.
Наиболее популярные форматировщики кода Python:
Большинство современных IDE также включают линтеры, которые работают в фоновом режиме по мере ввода текста и помогают выявлять мелкие ошибки кодирования, ошибки, опасные шаблоны кода и поддерживать форматирование кода. Существует два типа линтеров: логические и стилистические.
Наиболее популярные линтеры Python:
Дополнительную информацию о линтинге и форматировании кода можно найти в статье Python Code Quality .
Конвенция о наименованиях
Одним из важнейших аспектов написания чистого кода является соглашение об именовании. Всегда следует использовать осмысленные и раскрывающие намерения имена. Всегда лучше использовать длинные, описательные имена, чем короткие имена с комментариями.
au = 55 # Плохой пример
active_user_amount = 55 # Хороший пример
Мы рассмотрим больше примеров в следующих двух разделах.
Чистый код на Python: Руководство по написанию
Переменные
1. Используйте существительные для имен переменных
2. Используйте описательные имена
Другие разработчики должны понимать, что хранится в переменной, просто прочитав её имя.
# Плохой пример
c = 5
d = 12
# Хороший пример
city_counter = 5
elapsed_time_in_days = 12
3. Используйте произносимые имена
Если имя невозможно произнести, будет сложно объяснять алгоритмы вслух.
from datetime import datetime
# Плохой пример
genyyyymmddhhmmss = datetime.strptime('04/27/95 07:14:22', '%m/%d/%y %H:%M:%S')
# Хороший пример
generation_datetime = datetime.strptime('04/27/95 07:14:22', '%m/%d/%y %H:%M:%S')
4. Избегайте неоднозначных сокращений
Лучше длинное понятное имя, чем запутанное сокращение.
# Плохой пример
fna = 'Bob'
cre_tmstp = 1621535852
# Хороший пример
first_name = 'Bob'
creation_timestamp = 1621535852
5. Придерживайтесь единого словаря
Не используйте синонимы для одинаковых типов данных и концепций
# Плохой пример
client_first_name = 'Bob'
customer_last_name = 'Smith'
# Хороший пример
client_first_name = 'Bob'
client_last_name = 'Smith'
6. Не используйте «магические числа»
Заменяйте непонятные числа на переменные с осмысленными именами.
import random
# Плохой пример
def roll():
return random.randint(0, 36) # Что означает 36?
# Хороший пример
ROULETTE_POCKET_COUNT = 36
def roll():
return random.randint(0, ROULETTE_POCKET_COUNT)
7. Используйте имена из предметной области
Если у вас много разных типов данных и переменных с ними, что создает дополнительную сложность и вероятность запутаться– не бойтесь добавить суффиксы с типами данных.
# Хороший пример
score_list = [12, 33, 14, 24]
word_dict = {
'a': 'apple',
'b': 'banana',
'c': 'cherry',
}
# Плохой пример (неясен тип данных)
names = ["Nick", "Mike", "John"]
8. Избегайте избыточного контекста
Не дублируйте контекст, уже заданный классом.
# Плохой пример
class Person:
def __init__(self, person_first_name, person_last_name, person_age):
self.person_first_name = person_first_name
self.person_last_name = person_last_name
self.person_age = person_age
# Хороший пример
class Person:
def __init__(self, first_name, last_name, age):
self.first_name = first_name
self.last_name = last_name
self.age = age
Функции
1. Используйте глаголы для имен функций
2. Не смешивайте термины для одной концепции
Выберите одно слово и придерживайтесь его.
# Плохой пример
def get_name(): pass
def fetch_age(): pass
# Хороший пример
def get_name(): pass
def get_age(): pass
3. Пишите короткие функции
4. Одна функция — одна задача
Если в названии функции есть «и», разделите её на две.
# Плохой пример
def fetch_and_display_personnel():
data = # ...
for person in data:
print(person)
# Хороший пример
def fetch_personnel():
return # ...
def display_personnel(data):
for person in data:
print(person)
Совет: Если функция требует более 5 минут для понимания — перепишите её.
5. Минимизируйте количество аргументов
Количество аргументов должно быть сведено к минимуму. Будет хорошо, если ваша функция будет принимать 1–2 аргумента. Если вам требуется принимать в функции больше аргументов, вы можете создать объект конфигурации, который будет принимать в себя функция, или разделить её на несколько функций.
# Плохой пример
def render_blog_post(title, author, created_timestamp, updated_timestamp, content):
# ...
# Хороший пример
class BlogPost:
def __init__(self, title, author, created_timestamp, updated_timestamp, content):
self.title = title
# ... остальные параметры
def render_blog_post(blog_post):
# ...
6. Избегайте флагов в функциях
Обычно, наличие флагов в функции обозначает то, что функция выполняет несколько разных задач, либо имеют несколько вариантов поведения. Разбивайте такие функции на несколько, вместо использования булевых флагов.
# Плохой пример
def transform(text, uppercase):
return text.upper() if uppercase else text.lower()
# Хороший пример
def uppercase(text): return text.upper()
def lowercase(text): return text.lower()
7. Избегайте побочных эффектов
Функция не должна изменять внешние состояния (например, глобальные переменные).
Под побочными эффектами подразумеваются изменения, которые функция вызывает вне своей собственной области видимости. Они усложняют понимание кода, делают его непредсказуемым и сложным для тестирования.
total = 0 # Глобальная переменная
# Плохо
def add_to_total(x):
global total
total += x
add_to_total(5)
print(total)
# Хорошо: не изменяет входные данные, возвращает новый результат
def calculate_new_total(current_total: int, x: int) -> int:
return current_total + x
current_total = 0
current_total = calculate_new_total(current_total, 5)
print(current_total) # 5
Комментарии
Независимо от того, как сильно мы стараемся писать чистый код, в программе всё равно останутся части, требующие дополнительных пояснений. Комментарии позволяют быстро объяснить другим разработчикам (и себе в будущем), почему код был написан именно так. Важно помнить, что избыток комментариев может сделать код даже более запутанным, чем без них.
В чём разница между комментариями в коде и документацией?
Тип | Отвечает на вопрос | Для кого |
---|---|---|
Документация | Когда и Как | Пользователи |
Комментарии в коде | Почему | Разработчики |
Чистый код | Что | Разработчики |
Подробнее о различиях между комментариями в коде и документацией можно узнать в статье Documenting Python Code and Projects.
1. Не комментируйте плохой код — перепишите его
Комментарии в стиле # TODO: Переделать это позже
— помогут вам только на короткой дистанции. Скорее всего вы или ваши коллеги про негу забудете. Рано или поздно, кому-то придется поработать над данным кодом и скорее всего, ему потребуеться много времени, что бы понять происходящее в нём, а так же понять, что именно вы хотели в нем переделать.
2. Читаемый код не требует комментариев
Комментарии должны объяснять «почему», а не «что».
# Плохой пример (избыточный комментарий)
# Проверяет, существует ли пользователь с указанным ID
if not User.objects.filter(id=user_id).exists():
return Response({'detail': 'Пользователь не найден'})
3. Не добавляйте «шумные» комментарии
Не добавляйте комментарий ради комментария
# Плохой пример (бесполезный комментарий)
numbers = [1, 2, 3, 4, 5]
# Вычисляем среднее значение
average = sum(numbers) / len(numbers)
4. Используйте общепринятые форматы комментариев
В Python сообществе есть несколько общепринятых форматов docstrings
Формат Google
def add(a, b):
"""Складывает два числа.
Args:
a (int or float): Первое слагаемое
b (int or float): Второе слагаемое
Returns:
int or float: Сумма a и b
Raises:
TypeError: Если любой из аргументов не int или float
Examples:
>>> add(2, 3)
5
>>> add(2.5, 3.5)
6.0
"""
if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
raise TypeError("Оба аргумента должны быть числами")
return a + b
Формат reStructuredText
def add(a, b):
"""Складывает два числа.
:param a: Первое слагаемое (int или float)
:type a: int | float
:param b: Второе слагаемое (int или float)
:type b: int | float
:return: Сумма a и b
:rtype: int | float
:raises TypeError: Если любой из аргументов не int или float
Примеры использования:
>>> add(2, 3)
5
>>> add(2.5, 3.5)
6.0
"""
if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
raise TypeError("Оба аргумента должны быть числами")
return a + b
Формат NumPy/SciPy
def add(a, b):
"""Складывает два числа.
Parameters
----------
a : int или float
Первое слагаемое
b : int или float
Второе слагаемое
Returns
-------
int или float
Сумма a и b
Raises
------
TypeError
Если любой из аргументов не является числом (int/float)
Examples
--------
>>> add(2, 3)
5
>>> add(2.5, 3.5)
6.0
"""
if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
raise TypeError("Оба аргумента должны быть числами")
return a + b
5. Не оставляйте закомментированный код
Удаляйте отладочный и устаревший закомментированный код.
Декораторы, контекстные менеджеры, итераторы и генераторы
В данном разделе мы рассмотрим некоторые концепции и приемы Python, с помощью которых мы можем писать более качественный код
Декораторы
Декораторы — это невероятно мощный инструмент в Python, позволяющий добавлять дополнительную функциональность к функциям. По сути, они представляют собой функции, вызываемые внутри других функций. Их использование помогает следовать принципу разделения ответственности (Separation of Concerns, SoC) и делает код более модульным. Освойте их — и вы начнёте писать по-настоящему «питонический» код!
Допустим, у нас есть сервер, защищённый паролем. Мы можем либо проверять пароль в каждом методе сервера, либо создать декоратор и защитить методы сервера следующим образом:
def ask_for_passcode(func):
def inner():
passcode = input("Введите пароль: ")
if passcode == '1234':
func()
else:
print("Доступ запрещён")
return inner
@ask_for_passcode
def start_server():
print("Сервер запущен")
Теперь каждый раз при вызове функций start
или end
наш сервер будет запрашивать пароль.
Контекстные менеджеры
Контекстные менеджеры упрощают работу с внешними ресурсами, такими как файлы и базы данных. Чаще всего они используются в конструкции with
. Их главное преимущество — автоматическое освобождение памяти после выхода из блока кода.
Рассмотрим пример:
with open('file.txt') as file:
content = file.read()
# Файл закрыт автоматически
Без менеджера контекста код выглядел бы так:
file = open('wisdom.txt', 'w')
try:
file.write('Python — это круто.')
finally:
file.close()
Итераторы
Итератор — это объект, который содержит конечное количество значений. Он позволяет последовательно перебирать элементы.
Допустим, у нас есть список имён, и мы хотим пройти по нему. Можно использовать next(names)
:
names = ["Майк", "Джон", "Стив"]
names_iterator = iter(names)
for i in range(len(names)):
print(next(names_iterator))
Или воспользоваться улучшенным и более читабельным циклом:
names = ["Майк", "Джон", "Стив"]
for name in names:
print(name)
В подобных циклах избегайте имён переменных вроде
item
илиvalue
. Это усложняет понимание того, что хранится в переменной, особенно во вложенных циклах.
Генераторы
Генератор — это функция в Python, которая возвращает итератор вместо одного значения. Главное отличие от обычных функций — использование ключевого слова yield
вместо return
. Каждое следующее значение из итератора извлекается через next(generator)
.
Пример генератора, создающего первые n
кратных чисел для x
:
def multiple_generator(x, n):
for i in range(1, n + 1):
yield x * i
multiples_of_5 = multiple_generator(5, 3)
print(next(multiples_of_5)) # 5
print(next(multiples_of_5)) # 10
print(next(multiples_of_5)) # 15
Модульность и классы
Для лучшей организации кода разбивайте его на отдельные файлы и каталоги. В ООП-языках следуйте принципам: инкапсуляция, абстракция, наследование, полиморфизм.
Разделение кода на классы упрощает его понимание и поддержку. Нет строгих правил по длине файлов или классов, но старайтесь делать их компактными (желательно до 200 строк).
Структура проекта в Django — отличный пример организации кода:
awesomeproject/
├── main/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── blog/
│ ├── migrations/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
└── templates/
Django использует шаблон MTV (Model-Template-View), аналогичный MVC. Каждое приложение находится в отдельной директории, а каждый файл отвечает за конкретную задачу. Если проект включает несколько приложений, минимизируйте их взаимозависимость.
Тестирование
Качественный код требует тестов. Тестирование помогает находить ошибки до релиза. Тесты так же важны, как и продакшен-код, поэтому уделяйте им достаточно времени.
Подробнее о тестировании можно почитать в статьях Jan Giacomelli:
Тестирование в Python (англ.)
Современная TDD-разработка в Python (англ.)
Заключение
Писать чистый код сложно. Нет универсального рецепта — нужны опыт и практика. Мы рассмотрели стандарты и рекомендации, которые помогут улучшить код. Главный совет: будьте последовательны, пишите простой и тестируемый код.
Помните: Если код сложно тестировать — он сложен в использовании.