Skip to content

Latest commit

 

History

History
406 lines (280 loc) · 26.2 KB

File metadata and controls

406 lines (280 loc) · 26.2 KB

Стиль кодирования на Python

Содержание

  1. Назначение
  2. Основные правила стиля кодирования
  3. Заключение

Зачем

Отсутствие стиля кодирования может породить множество проблем:

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

Источники

При написании кода следует руководствоваться конвенцией 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.