- Назначение
- Основные правила стиля кодирования
- Длина строк
- Кавычки
- Отступы
- Оформление докстрок
- Аннотации типов
- Именованные параметры
- Ассерты
- Количество параметров функции и методов
- Строки
- Ожидания
- Таймауты
- Комментарии
- Вывод информации
- Инкапсуляция
- Импорты
- Неиспользуемые импорты и переменные
- Именование сущностей
- Переносы строк кода
- Константы
- Обработка исключений
- Списковые включения
- Заключение
Отсутствие стиля кодирования может породить множество проблем:
- Усложнения восприятия при чтении участков кода, написанных и отформатированных в разном стиле. Усложняется переход между проектами.
- Возможно появление большого количества изменений в коде, связанных только с форматированием
- Потенциальные споры при возникновении разногласий о форматировании кода
При написании кода следует руководствоваться конвенцией Python, если правило не переопределено ниже.
Следует использовать максимальную длину строки 110 символов.
Почему так? На двух открытых вкладках редактора на мониторе FullHD помещаются строки с максимальной длиной в 110 символов.
Следует использовать одинарные кавычки для задания строковых литералов:
string = 'Это строка'Почему так? У одинарных кавычек можно выделить несколько преимуществ:
- для набора требуется нажатие только на одну клавишу, а не на две, как для двойных
- более читаемый вид в коде, легче разобрать глазами
- удобно использовать при формировании строк с двойными кавычками: JSON, css- и xpath-селекторы
- pytest при выводе использует одинарные кавычки
Следует использовать отступ в 4 пробела. Не следует использовать табуляцию.
Почему так? Табуляция может некорректно восприниматься некоторыми текстовыми редакторами.
Докстроки следует оформлять в стиле Google. Для автоматической генерации докстрок можно использовать плагины, например, autodocstring для VSCode. В конце первой строки докстроки должна быть точка (добавляется автоматически линтером).
def count_boxes(boxes: list[Box]) -> int:
"""Подсчет количества коробок.
Функция считает количество коробок в списке
Args:
boxes: Список коробок.
Returns:
Количество коробок (int)
Raises:
ValueError: Ошибка при количестве коробок больше 1000000.
"""Почему так: согласно конвенции Python
Аннотации типов следует указывать всегда в параметрах функций, возвращаемых значениях, а также там, где при определении переменной ее тип сложно понять. Для упрощения восприятия сложных типов лучше создавать специальные типы с помощью TypeAlias.
from typing import TypeAlias
MyType: TypeAlias = dict[str, list[int]]
my_dict: MyType = {'a': [1, 2, 3]}Допускается не указывать аннотации для параметров, тип которых не ясен или не может быть точно определен (например, при использовании сторонней библиотеки).
При создании сложных типов следует использовать обновленный синтаксис c |, не следует использовать Union и Optional:
a: int | bool = 0
b: str | None = NoneПочему так: аннотации типов позволяют линтерам уже в процессе написания кода подсвечивать ошибки, код легче читать, так как видно, аргумент какого типа ожидается.
При количестве параметров функции или метода больше 1 следует использовать именованные параметры.
# сложно понять, какой параметр к чему относится, легко перепутать местами аргументы
my_func(a,b,c)
# легче читать
my_func(length=a,width=b,height=c)Почему так: это позволяет лучше видеть при вызове функции/метода какое значение принимает какой аргумент, а также позволяет безопасно проводить рефакторинг: добавлять новые аргументы, менять их порядок в сигнатуре
Для ассертов следует использовать нативный assert или библиотеку assertpy. При использовании assertpy рекомендуется избегать экзотических матчеров, для исключения ошибок и лучшего понимания коллегами. Также следует избегать слишком длинных цепочек. Цепочка должна соответствовать одной логической проверке. Например: проверка добавления элемента в список:
actual_items = ['12345', '123456']
expected_length = 2
expected_item = '12345'
assert_that(actual_items).contains(expected_item).is_length(expected_length)При необходимости использования особенной проверки рекомендуется создать специальный матчер. Рекомендуется задавать сообщение при неуспешной проверке или выносить его в шаг репортера. Рекомендуется использовать софт-ассерты (неблокирующие ассерты) для множественных проверок. Это позволит за один прогон тестов определить все проваленные проверки. Если требуется проверить много полей в объекте, то рекомендуется использовать сравнение объектов моделей (dataclass, pydantic) или словарей данных. Таким образом будет достаточно только одной проверки вместо множества.
Почему так: assert прост и не требует дополнительных библиотек, assert_that из assertpy обладает ясным синтаксисом (принимает только один параметр - фактическое значение), не требует дополнительных импортов матчеров, обладает fluent-интерфейсом (проверки можно выполнять цепочкой). Также возможно добавления собственных матчеров. Софт-ассерты или сравнение объектов позволят сразу определить несколько проваленных проверок за один прогон теста
Количество параметров функции должно быть минимальным для обеспечения ее функциональности и переиспользования. При количестве аргументов функции больше 3 или если сигнатура превышает длину строки рекомендуется каждый аргумент объявлять на отдельной строке
# сложно читать код
def my_func(length=0, width=0, height=0, weight=0) -> None:
print(f'Length: {length}, Width: {width}, Height: {height}')
# более понятная сигнатура функции
def my_func2(
length=0,
width=0,
height=0,
weight=0,
) -> None:
print(f'Length: {length}, Width: {width}, Height: {height}')Однако при большом количестве параметров с такой функцией становится сложно работать. Поэтому рекомендуется при количестве параметров функции больше 3х использовать ValueObject - объект, содержащий необходимые данные (датакласс, pydantic-модель).
from dataclasses import dataclass
@dataclass
class Box:
length_mm: int
width_mm: int
height_mm: int
weight_kg: int
def my_func3(box: Box) -> None:
print(f'Length: {box.length_mm}, Width: {box.width_mm}, Height: {box.height_mm}, Weight: {box.weight_kg}')Почему так: такой подход позволяет легче читать код, передавать сразу все данные объекта целиком
Для объявления строковых литералов следует использовать одинарные кавычки. Для форматирования строк следует использовать f-строки. Длинные строки следует разбивать на части с помощью скобок, не следует для этого использовать символ "".
# не следует делать
long_string = "Это очень длинная строка которую нужно \
перенести на несколько строк для читаемости"
# следует делать (PEP 8 style):
long_string = (
"Это очень длинная строка которую нужно "
"перенести на несколько строк для читаемости"
)Почему так: f-строки имеют понятный и простой синтаксис, а также работают быстрее других видов форматирования.
Необходимо избегать статических (фиксированных) ожиданий. Вместо них следует использовать повторения выполнения действия (динамические ожидания, поллинг) или колбэки. Статические ожидания могут являться вынужденной мерой, если другое решение в текущий момент неприменимо или сложно реализуемо. В этом случае такое ожидание следует сопровождать комментарием, для чего оно было сделано, почему нельзя было сделать иначе.
# Ожидание сообщения в очереди кафки. Не хватило времени для написания метода с повторами.
time.sleep(2)Для динамических ожиданий рекомендуется применять готовые библиотеки, например, retrying или tenacity.
Почему так: это позволит повысить надежность и скорость тестов. При рефакторинге комментарий будет полезен для понимания причины проблемы и возможности замены на динамическое ожидание
Все сетевые взаимодействия должны иметь таймауты: http, grpc, обращения к БД и kafka. Во многих библиотеках python по умолчанию таймаутов нет, то есть нет ограничений по времени на выполнение запроса (requests, httpx, sqlmodel). В противном случае (при зависании запроса на неопределенное время результаты прогона могут быть потеряны. Настройки таймаутов должны читаться из конфигурации тестов.
Почему так: важно недопустить зависание прогона и неполучения данных о результатах тестов.
Нужно стараться писать код, который понятен без комментариев. Однако в случаях неочевидного решения, проблемы в библиотеке или каких-то неочевидных причин или сложности алгоритма следует добавлять комментарий. Это позволит коллегам или автору в будущем быстрее разобраться в проблемах или сделать рефакторинг. Комментарии рекомендуется делать строкой выше комментируемого кода.
# это комментарий для строки с print
print('Hello')Не рекомендуются многострочные комментарии за исключением докстрок.
Почему так: наличие комментариев к сложному или неочевидному коду позволит проще поддерживать код. Комментарий, который начинается с начала строки сложнее пропустить, он не будет скрыт за краем окна редактора
В коде не должно быть команд print для вывода информации. Вместо этого следует использовать логгеры или репортеры. Print-выражения затрудняют отладку и контроль вывода приложения, а также могут привести к утечке информации в production-среде.
Почему так: использование логгеров позволяет контролировать уровень детализации вывода, фильтровать сообщения по важности, а также направлять вывод в нужные каналы (файлы, консоль, внешние системы). Репортеры тестрана позволяют структурировать вывод тестов для лучшего понимания результатов.
Для удобства и повышения надежности кода следует использовать инкапсуляцию. Названия защищенных атрибутов должны начинаться с '_', а приватных с '__'.
Почему так: правила именования соответствуют стандартным рекомеднациям кодирования на Python. Инкапсуляция помогает писать более безопасный и поддерживаемый код.
Следует использовать абсолютные импорты. Не допускаются импорты вида
from module import *Рекомендуется импортировать модуль целиком, если это возможно. Обращение через module_name.var или module_name.func делают код более понятным (сразу ясно, что переменная/функция не локальная). Рекомендуется применять автоматическую сортировку импортов. Импорт следует осуществлять из модуля, где объявлен нужный объект, а не из модуля, где этот объект уже импортирован.
Почему так: это позволяет снизить вероятность появления циклических и повторных импортов, а также делает код более понятным и стандартизированным.
В коде не должно быть неиспользуемых импортов и переменных. Все импорты и переменные должны использоваться в коде. Неиспользуемые элементы следует удалять, чтобы поддерживать чистоту кода и избегать путаницы.
Почему так: неиспользуемые импорты и переменные создают визуальный шум, затрудняют чтение и понимание кода, а также могут привести к ненужной нагрузке на систему. Удаление неиспользуемых элементов делает код более чистым и понятным.
Именование сущностей - одна из важнейших частей кодирования. При чтении кода должно быть точно понятно назначение каждой сущности. Переменные типа bool следует именовать, начиная с "is_". Это выделяет их из всех переменных и дает понимание, что переменная хранит True или False.
is_paid: bool = False
is_photo_required: bool = TrueИмена переменных других типов должны отвечать на вопрос "Что это?". При этом в них не должно содержаться предлогов
# плохо
count_of_postings: int = 0
# xорошо
postings_count: int = 0Имена функций и методов должны начинаться с глагола, обозначая, что сущность выполняет действие:
create_draft(draft_data: DraftDataModel) -> DraftData: ...
make_payment(money_amount_rub: int) -> None: ...Если переменная имеет единицы измерения, то рекомендуется указывать их в конце переменной, это ускоряет чтение кода:
timeout_ms: int = 1000
posting_length_mm: int = 0В именах переменных, имена которых совпадают с ключевыми словами python следует добавлять символ "_" в конце названия:
from_: Any
id_: intПочему так: согласованное именование сущностей позволяет быстрее разобраться в коде, быстрее писать новый код, проще отлаживать ошибки
Переносы строк при слишком большой длине строки кода следует делать с помощью скобок. Не следует делать явные переносы строк с помощью "". При разрыве строки код будет лучше читаться, если оператор будет следовать перед операндом. Форматирование следует осуществлять автоматически с помощью линтеров и форматтеров (ruff).
# не следует делать
result = first_value * 100 + \
second_value * 200 + \
third_value * 300
# следует делать (PEP 8 style):
result = (
first_value * 100
+ second_value * 200
+ third_value * 300
)Почему так: следование стандарту pep 8, легче читается код, автоформатирование избавляет от необходимости задумываться об этом
Для определения констант следует использовать заглавные буквы с разделением слов символом подчеркивания.
# правильно
MAX_OVERFLOW = 1000
PI = 3.14159
# неправильно
max_overflow = 1000
pi = 3.14159Почему так: это стандартное соглашение в Python, которое позволяет легко отличить константы от переменных.
При обработке исключений следует использовать конкретные типы исключений, а не общее Exception. Не следует игнорировать исключения с пустым блоком except.
# правильно
try:
value = int(user_input)
except ValueError:
logger.error("Некорректный формат числа")
value = 0
# неправильно
try:
value = int(user_input)
except:
passЕсли необходимо перехватывать несколько типов исключений, их следует указывать в кортеже:
try:
value = int(user_input)
except (ValueError, TypeError):
logger.error("Некорректный формат числа")
value = 0Почему так: это позволяет более точно обрабатывать ошибки и избегать сокрытия неожиданных проблем в коде.
Списковые включения следует использовать для создания списков, когда выражение простое и понятное. Для сложных выражений следует использовать обычные циклы for.
# правильно
squares = [x**2 for x in range(10)]
even_numbers = [x for x in range(10) if x % 2 == 0]
# неправильно для сложных выражений
matrix = [[i*j for j in range(5)] for i in range(5)] # слишком сложноЕсли выражение в списковые включения занимает более одной строки, следует использовать обычный цикл for.
Почему так: это делает код более читаемым и понятным, также списковые включенияs выполняются быстрее, чем обычные циклы for.
Для проверки и автоматического исправления стиля кодирования следует применять линтеры и форматтеры. Чтобы проверки запускались автоматически, можно использовать pre-commit hook в git.