BakinSergey / codestyle

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Python Code Style Guide

Питон

1 2 3 4 5 6 7 8 9 10
S1 S2 S3 S4 S5 S6 S7 S8 S9 S10
S11 S12 S13 S14 S15 S16 S17 S18 S19 S20
S21 S22 S23 S24 S25 S26 S27 S28 S29 S30
S31 S32 S33 S34 S35 S36 S37 S38 S39 S40

Логирование

1 2 3 4 5 6 7 8 9 10
L1 L2 L3 L4 L5 L6 L7 L8 L9 L10

Функции

1 2 3 4 5 6 7 8 9 10
F1 F2 F3 F4 F5 F6 F7 F8 F9 F10

Содержание

S0. Порядок объявлений в модуле

  1. import
  2. специальные переменные
  3. константы
  4. переменные
  5. функции
  6. классы

S1. Константы, переменные, функции и классы могут быть внутренними (internal, имя начинается с _) и публичными. В этом случае внутри каждого блока также должен соблюдаться порядок - сначала внутренние, затем публичные. Изменение порядка и смешивание разных блоков не допускается.

Исключение: допускается объявление переменных в конце модуля, если они вызывают функцию или создают объект класса, объявленные выше.

Пример:

import requests

__all__ = ['THING_LIMIT', 'let_me_google_it_for_you', 'Anything']

_GOOGLE_URL = 'https://google.com'
THING_LIMIT = 10

_search_count = 0


def _get_search_url(term):
    return '{}?q={}'.format(_GOOGLE_URL, term)


def let_me_google_it_for_you(query):
    _search_count += 1
    response = requests.get(
        _get_search_url(query)
    )
    return response.text()


class _Something:
    pass


class Anything(_Something):
    pass

S2. Порядок объявлений в классе

  1. константы
  2. свойства класса(поля)
  3. @property
  4. специальные методы класса
  5. методы класса

S3. Константы, свойства и методы класса могут быть внутренними (internal, имя начинается с _) и публичными. В этом случае внутри каждого блока также должен соблюдаться порядок - сначала внутренние, затем публичные. Изменение порядка и смешивание разных блоков не допускается.

Пример:

class Anything:
    _APPLE = 1
    _BEEF = 2

    FRUIT = 'fruit'
    MEAT = 'meat'

    _default = FRUIT
    use_default = False

    @property
    def stuff(self):
        return None

    @stuff.setter
    def stuff(self, v):
        print(v)

    def __init__(self, thing):
        self._thing = thing or self._default

    def _transform(self):
        self._thing = next(thing for thing in [self.FRUIT, self.MEAT] if thing != self._thing)

    @staticmethod
    def blow_up():
        exit(2)

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

S4. Все настройки, которые приложение получает извне(аргументы запуска, переменные окружения) - это конфигурация приложения. Конфигурация должна быть оформлена должным образом. Это м.б. файл config.py, либо другое решение, исполняемое 1 раз, при старте приложения, так чтобы в дальнейшем не было необходимости обращаться к извне. Нотификационные и конфигурационные события, изменяющие логику работы в райнтайме должны взаимодействовать с объектом конфигурации приложения, а не изменять его стартовое окружение.

S4.1. Данные в .env файлах это строки содержащие пару атомарных значений: "КЛЮЧ"="ЗНАЧЕНИЕ" в верхнем регистре. Должны импортироваться и валидироваться ТОЛЬКО стандартными библиотеками. Самописный парсинг строковых значений не допускается. .env-файл - максимально глупая сущность.

S5. Для каждого параметра конфигурации должно быть значение по-умолчанию с возможностью его переопределить.

S6. Всё что приложение получает извне - переменные окружения, аргументы командной строки, должны быть описаны в файле README, в таблице "Параметры приложения", указываются: кодовое имя, назначение, пример значения(либо набор допустимых значений), тип значения,
условия валидности, дефолтное значение, источник(args, env, vault), значимые примечания.

Примеры параметров приложения:

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

Глобальные переменные

S7. Использование глобальных переменных не допускается.


Naming

S8. Не следует выбирать бессмысленных имен, например abc, x, temp и пр. Так же не следует сокращать названия (полностью или частично), например e или el вместо element. Исключение: счетчики в циклах for i, item in enumerate(items) и list comprehension objects = [x[0] for x in get_elements()]

Список допустимых сокращений(не ухудшающих читаемость):

  • msg (message)
  • elem(element)

Общее исключение: в сложном коде, где имена переменных не влияют на читаемость(алгоритмы) нейминг произвольный. Такие функции помечаются как #pylint: disable=invalid_name, либо другим коментарием

Стараться выбирать говорящие имена, но не слишком абстрактные (типа manager-ов и helper-ов), четко описывающие назначение функции или переменной. Имя должно быть максимально коротким, ёмким и говорить о том, что делает функция\метод\класс\etc. При этом не следует давать очень длинные названия. Если какие-то подробности в названии можно опустить и суть останется понятна - лучше их опустить.

S9. Избегать похожих имен, которые описывают совершенно разный функционал,либо затеняют builtins(что то вроде pass_, str_).

Классы

Классы абстрагируют действие либо объект, либо и то и другое(менеджеры).

S10. Имя класса, абстрагирующего объект должно быть существительным и отвечать на вопрос что? В общем случае - чем абстрактнее сущность тем абстрактнее и имя для класса сущности. Account, Card, TableRow, Entry.

S11. Имя класса, абстрагирующего действие должно начинаться с глагола, содержать объект действия и максимально точно отвечать на вопрос что и с чем я делаю? GetCardData, DelAccount

S12. Имя класса, абстрагирующего и объект и действия над ним должно быть максимально коротким и при этом максимально понятным: ThreadPoolExecutor, PdfPrinter, TaxCalculator, ExcelClient

Функции (методы класса)

S13. Функции выполняют какое-либо действие, причем только одно, и их название начинается с глагола и описывает совершаемое действие: get_key, configure, login Так же они могут выполнять какую-либо проверку, в таком случае их название должно подразумевать однозначный ответ на вопрос (да\нет) и начинаться с is, has, can, does и т.п.

Если не получается придумать короткое и ёмкое название для функции, то скорее всего она делает гораздо больше, чем следует. Возможно, следует заняться рефакторингом и разбить её на функции поменьше.

S14. Не допускается использовать отрицательный контекст в нейминге. Например IsNotValid(нужно IsValid)

Переменные (свойства класса)

S15. Если переменная содержит значение булева типа, то её название так же должно подразумевать однозначный ответ на вопрос (да\нет) и начинаться с is, has, can, does и т.п.


Описание публичных интерфейсов

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

S17. Докстринг - это строка максимально короткая и максимально понятная, на русском языке.

S18. Не допускается описывать конкретную реализацию (каким именно образом решается проблема).

S19. Шаблон докстринга для повторно используемого кода, РАСПРОСТРАНЯЕМОГО КАК ПИТОН ПАКЕТЫ. В описании должно быть:

  • Назначение (какую проблему решает этот класс\функция)
  • Примеры использования (опционально)
  • Описание аргументов, которые принимает функция или конструктор класса
  • Исключения, которые могут быть выброшены в процессе работы функции
  • Результат (или возвращаемое значение)
def do_something(param1: int, param2: str) -> bool:
    """
    Назначение. Какую проблему решает?

    Args:
        param1: Первый параметр. Принимаер разные значения, например:
            1: Число. Первое после нуля.
            2: Должно быть понятно.
            `SOME_CONSTANT`: Когда не понятно, но очень надо.
        param2: Второй параметр.

    Raises:
        ValueError: Некорректное значение param1.
         CustomException: Есть свои причины.

    Returns:
        Описание возвращаемого значения.

    [опционально]Example:
        ```something = do_something(param1, param2)```

    """

Type hinting

S20. PEP 484 (классы и функции) - применяется везде согласно спецификации. Уточнение 1: Версия 3.9 допускает использование в качестве хинтов простых типов - классов встроенных типов(dict, tuple и др.). Для единого стиля хинтов использовать только типы из Typing.

Недопустимо:

def f(a: dict):
    pass

Нужно:

def f(a: Dict):
    pass

S21. PEP 526 (переменные) - не обязательно и применяется на усмотрение разработчика.

Недопустимо:

def f(a):
    pass

Плохо:

from typing import Text, Union

def f(a: Text):
    pass
import typing

def f(a: typing.Text):
    pass

Хорошо:

from typing import *

def f(a: Text):
    pass

Отлично:

from typing import *

def check_text(a: Text) -> Bool:
    pass

Работа со строками

S22. В приоритете использовать f-строки. Форматировать строки через str.format предпочтительнее, чем через %. Но делать это следует только тогда, когда есть необходимость подставить значения в шаблон.

S23. Для конкатенации строк использовать оператор +, когда их 2, если больше - str.join

Плохо:

value = '{}{}{}'.format(x, y, z)
print('I %s code review!' % 'love' if True else 'hate')

Хорошо:

requests.get('http://my.api.com' + '/handle', params)
value = ''.join([x, y, z])
print('I {} code review!'.format('love' if True else 'hate'))
# or
print(f'I {value} code review!')

Закомментированный код

S24. В коде MR не должно быть ни одной строчки закомментированного кода. Все что не нужно - должно быть удалено - в т.ч. коментарии, поясняющие понятный код, ToDo. Если хочется оставить что-то для примера, то лучше вынести это в описание в docstring. Чтобы не забыть что то(ToDo) - можно испльзовать закладки в IDE Примечание: это не про комментарии к коду, их можно.

Допускается использовать коментарии, визуально отделяющие И ОПИСЫВАЮЩИЕ начало какой-либо группы функций класса/модуля. Цель - облегчить ориентацию в большом количестве функций. (например #=================== взаимодействие с брокером ======================).


Неиспользуемые переменные

S25. Когда функция возвращает больше значений, чем нужно (то есть они не будут использоваться в коде), не следует давать названия переменным с такими значениями.

Плохо:

a, b, c = get_names()

Лучше:

a, _, __ = get_names()

Совсем хорошо:

a, *_ = get_names()

S26. Не допускается использование _ как параметра в сигнатуре функции.

Недопустимо:

def check_text(_: Str, __:Bool) -> Bool:
    pass

Допустимо:

from utils import my_util_fn as _ 

Условные выражения

Тернарный оператор

S27. Вариант записи через тернарный условный оператор является более предпочтительным.

Плохо:

if user.has_name():
    x = 5
else:
    x = 0

Хорошо:

x = 5 if user.has_name() else 0

Отрицательный контекст

S28. Проверяемые перeменные и функции не должны оперировать отрицательным контекстом.

Плохо:

has_no_accounts = len(User.accounts) == 0
if has_no_accounts:
    pass

Хорошо:

has_accounts = bool(User.accounts)
if not has_accounts:
    pass

Большие условия

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

Плохо:

if ((user.is_admin or user.can_access()) and (len(sections) or items)) or config.get('force_show', False):
    pass

Хорошо:

can_access = user.is_admin or user.can_access()
page_is_empty = not sections and not items
force_show = config.get('force_show', False)

if (can_access and not page_is_empty) or force_show:
    pass

Свойства классов

S30. Не использовать геттеры и сеттеры для свойств класса, а объявлять их публично. В случае, когда нужны вычисления, использовать декоратор @property.

Плохо:

class Something:
     _x = 0
    _anything = None

    def set_anything(self, v):
        self._anything = v

    def get_anything(self):
        return self._anything

    def set_x(self, v):
        self._x = v * 2

    def get_x(self):
        return self._x

Хорошо:

class Something:
    _x = 0
    anything = None

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, v):
        self._x = v * 2

True/False

S31. Использовать явные приведения к булеву типу там, где это необходимо(т.е чтобы кастовать только в одном месте).

Плохо:

class Box:
    things = []

    def has_something(self):
        return self.things

Хорошо:

class Box:
    things = []

    def has_something(self):
        return bool(self.things)

filter, map, reduce

S32. Плохо читаемые конструкции, от которых предпочтительнее отказаться в пользу list\dict comprehension.

Плохо:

map(lambda x: x[1], filter(lambda x: x[2] == 5, my_list))

Хорошо:

[item[1] for item in my_list if item[2] == 5]

Импорт

Структура проекта для справки:

app/
 |- __init__.py
 |- main.py
my_package
 |- __init__.py
 |- my_module.py
 |- utils.py

Что импортировать

S33. В приоритете импортировать только пакеты или модули. Допустимо импортировать классы, функции и переменные напрямую с ограничением: такой импорт занимает не более 5 строк, либо 10 имен. Недопустимо импортировать неявным образом (from module import *).

Исключение: для модуля `typing` допускается делать `from typing import *` или импортировать классы напрямую `from typing import Type, AnotherType`

Плохо:

from datetime import datetime, timedelta  # импорт классов
from decimal import Decimal
from urllib import parse

datetime.now() - timedelta(days=7)
parse.url_parse('http://www.ru/url')

Хорошо:

import datetime  # импорт модуля
import decimal
import urllib.parse

datetime.datetime.now() - datetime.timedelta(days=7)
urllib.parse.url_parse('http://www.ru/url')

Как импортировать

S34. При импорте использовать только абсолютные пути.

app/main.py:

import my_package
# или
import my_package.my_module

S35. В случае, когда у импортируемого модуля\пакета получается очень длинный путь, допускается сокращать пути с сохранением смысла используя конструкцию import as. Сокращенное название по-прежнему должно давать четкое понимание что это за модуль и откуда оно взялось. Для этого следует составлять новое название из компонентов полного пути (обычно первый+последний, иногда вставляются промежуточные для большей ясности).

Например:

import django.db.models as django_models
import django.core.management.commands.flush as django_commands_flush

Импорт в контексте пакета

S36. При импорте собственных модулей внутри пакета использовать конструкцию from root.package import submodule, где root.package - полный путь до пакета, submodule - модуль этого пакета.

my_package/my_module.py:

from my_package import utils

import *

S37. Допускается использовать конструкцию from package.submodule import * только в __init__.py При этом импортируемый модуль должен описывать свой публичный интерфейс в переменной __all__

Используйте этот подход для определения простых и понятных интерфейсов, чтобы избегать длинных цепочек импорта: import masterpiece.xyz.contrib.that_lib.awesome_feature

app/my_package/__init__.py:

from my_package.my_module import *

my_package/my_module.py:

__all__ = ['MyClass']

def _do_stuff():
    pass

class MyClass:
    pass

Логирование

по мотивам

Что записывать в лог ? Программа — это серия переходов между состояниями. Состояния — это вся информация, которую программа хранит в своей памяти в определенный момент времени, а код программы определяет то, как она переходит от одного состояния к другому. Логи должны содержать информацию, необходимую для реконструкции переходов состояний. Невозможно, да и не нужно, фиксировать все состояния во все отрезки времени.

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

L1. Лог должен быть значимым.

Хорошо:

logger.info("start rest handler process")
logger.info(f"reconnecting to database, attemp{cnt}, next after {rec_interval} sec")

Плохо:

logger.info("in get_data function")
logger.info(f"{meaningful_var=}")

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

L2. Лог должен содержать контекст - модуль, время, другие значимые данные о состоянии. Для вывода модуля при логировании в проде разумно использовать уникальные в рамках проекта сокращения [TL][MPR] минимальной длины Формирование таких акронимов можно делать авотматически, в классе логгера, используя маппинг по значению переменной module

L3. Лог должен быть структурирован и располагаться на разных, соответствующих уровнях.

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

Неправильно:

def validateSSN(ssn: Str): 
  
  regex = "^(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-?!0000)[0-9]{4}$"
  pattern = re.compile(regex)
  matcher = pattern.matcher(ssn)

  if !matcher.matches():    
    logger.info("Bad SSN blah, blah, blah...")
    raise ValidationException("expecting SSN format AAA-GG-SSSS but got %s", ssn.replaceAll("\\d", "*"))

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

def validateUserUpdateRequest(UserUpdateRequest req): {
    ... проверка другого атрибута req 
 
    try:
       validateSSN(req.ssn);
    except ValidationException as exc:        
       logger.info("Received a user update request(track id %s) from user uuid %s, rejecting it because %s", req.trackID, req.uid, e.getMessage()));
       ... другая логика обработки ошибки ...

L4. Лог должен быть сбалансированным: он не должен содержать слишком мало или слишком много информации.

L5. Лог должен быть форматированным: регистрация сообщений должна быть понятна людям и просто разбираться машинами.

L6. Лог в сложных приложениях должен вестись в несколько файлов журнала.

L7. Лог должен иметь стратегию ротации.

L8. Лог должен быть адаптирован к разработке и продакшену.

L9. Сообщение продакшен лога должно быть максимально коротким и информативным.

Хорошие функции

по мотивам

F1. Внятное название. Максимально короткое и максимально понятное.

У разработчика вообще не знакомого с кодом, только из названия должно сложиться первичное понимание назначения ф-ии. Чем выше вероятность того что это первичное понимание верное - тем правильнее названа функция. (В идеале: дворник к-й изучал английский в школе - без проблем переводит название функции и дает пояснения по возможной реализации).

F2. Соответствует принципу единственной обязанности.

F3. Сигнатура оформлена по PEP 484

F4. Содержит докстроку на русском языке. Про докстроки смотреть S16, S19

F5. Состоит не более чем из 50 строк. Исключение - чисто алгоритмические истории например

F6. Она идемпотентная и, если это возможно, чистая

F7. Максимальное суммарное(включая декораторы декораторов) кол-во декораторов конкретной функции - 2. Первый декоратор - каким либо образом группирует функции клиентского кода, Второй - про модификацию поведения.

F8. В случаях когда это позволяет понизить вложенность кода(число отступов) - функции должны следовать early return pattern - стратегии, когда функция возвращает результат сразу же, а не в конце,

About