python

Небанальные правила чистого Python. Часть 1

  • вторник, 1 ноября 2022 г. в 01:02:18
https://habr.com/ru/post/693668/
  • Python
  • Проектирование и рефакторинг


Большинство питонистов не раз слышали о таких правилах как «функции должны быть глаголами» или «не наследуйтесь явно от object в Python 3». В этой статье мы рассмотрим не такие банальные, но полезные правила чистого кода в Python.

Необязательное вступление

Идея статьи возникла при выполнении Code review одного проекта. В тот момент я понял что пора объединить и структурировать накопленные правила чистого кода.

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

Функции

Правило №1 - Имя начинается с нижнего подчёркивания, если функция используется только в том модуле, в котором она создана

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

Например в проекте есть модули «a.py», «b.py» и «c.py». Функция get_user_name создана в модуле «a.py». Используется она тоже только в нём. Тогда её следует переименовать в _get_user_name.

Правило №2 - Примеры использования функции в docstrings пишутся в виде doctest

Напишем такую функцию:

def get_sum(number_1: int, number_2: int) -> int:
    """Вернёт сумму двух чисел.

    Примеры:
    get_sum(0, 2) = 2
    get_sum(1, 2) = 3
    get_sum(3, 5) = 8

    """

    return number_1 + number_2


print(get_sum(10, 15))

Функция работает, но запустив код, мы никак не проверим примеры из docstring:

# Флаг «v» выводит дополнительные детали выполнения программы
$ python script.py -v
25

Исправим это с помощью модуля doctest:

from doctest import testmod


def get_sum(number_1: int, number_2: int) -> int:
    """Вернёт сумму двух чисел.

    >>> get_sum(0, 2)
    2
    >>> get_sum(1, 2)
    3
    >>> get_sum(3, 5)
    8

    """

    return number_1 + number_2


if __name__ == "__main__":
    print(get_sum(10, 15))

    testmod()

Теперь запустим программу:

$ python script.py -v
25
Trying:
    get_sum(0, 2)
Expecting:
    2
ok
Trying:
    get_sum(1, 2)
Expecting:
    3
ok
Trying:
    get_sum(3, 5)
Expecting:
    8
ok
1 items had no tests:
    __main__
1 items passed all tests:
   3 tests in __main__.get_sum
3 tests in 2 items.
3 passed and 0 failed.
Test passed.

Мы получили результат работы программы и результат выполнения тестов из docstring. Уберите флаг «v», если хотите вывести только результат работы программы:

$ python script.py
25

Правило №3 - У аргументов функции указан type hint

Взгляните на эту функцию:

def is_user_name_valid(user_name): pass

Какое значение нужно передать в переменную user_name? Строку с именем? Словарь с ФИО? Может ещё что-то? Скорее всего строку с именем, но для полной уверенности надо читать саму функцию. Type hint освобождает от этой траты времени:

def is_user_name_valid(user_name: str): pass

Плюсы использования type hint:

  1. Позволяет не думать над типом аргумента;

  2. Немного документирует код;

  3. Уменьшает число ошибок, связанных с типом аргумента;

  4. Облегчает разработку в некоторых IDE. Например PyCharm может ругаться на аргумент, который не соответствует type hint.

Type hint для аргументов по умолчанию

Для аргументов по умолчанию тоже можно задать type hint:

def is_user_name_valid(user_name: str = "admin"): pass

Особенно это полезно если аргумент может принимать значения разных типов:

# Для Python 3.10
def is_positive(number: int | float = 100): pass


# Для Python 3.9 и ниже
from typing import Union

def is_positive(number: Union[int, float] = 100): pass

Type hint для переменных

Для переменных тоже можно указать type hint. Но нет смысла это делать, если тип переменной и так понятен.

Плохо:

cat_name: str = "Tom"

Хорошо:

# settings.PAGE_SIZE может иметь значение разных типов, например str и int
page_size: int = settings.PAGE_SIZE

Правило №4 - У функции указан type hint возвращаемого значения

Type hint полезен не только для аргументов и переменных, но и для возвращаемого значения функции. За счёт него можно не заглядывать в тело функции, а сразу понять какой тип она вернёт.

from typing import Callable


def get_user_name() -> str: ...

def is_user_name_valid(user_name: str) -> bool: ...

def get_wrapped_function() -> Callable: ...

def run_tests() -> None: ...

У функции, которая возвращает другую функцию, указывается type hint Callable. У функции, которая ничего не возвращает, указывается type hint None.

Классы

Правило №5 - Приватные методы располагаются ниже магических и публичных

Допустим, есть такой кот класс:

class Cat:
    """Просто кот"""

    def __init__(self, name: str):
        self.name = name

    def ask_for_food(self) -> None:
        self.__say_meow()
        self.__say_meow()

    def __say_meow(self) -> None:
        print(f"{self.name} says meow")

Мы создаем его объект и вызываем публичный метод:

tom = Cat("Tom")
tom.ask_for_food()

Если человек захочет понять что делает метод ask_for_food, то он прочитает содержимое класса Cat в таком порядке:

  1. Прочитает метод __init__ и поймёт куда заносится имя "Tom";

  2. Прочитает метод ask_for_food и увидит в нём вызов метода __say_meow;

  3. Прочитает метод __say_meow.

Т.е. приватный пользовательский метод читается в последнюю очередь. Так всегда происходит со всеми не магическими private-методами, если в коде соблюдаются принципы ООП.

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

  1. __new__ (если такой метод используется в классе);

  2. __init__;

  3. Остальные магические методы;

  4. Public-методы;

  5. Protected-методы;

  6. Private-методы.

Переменные

Правило №6 - Названия переменных, в которых хранятся измеряемые данные, содержат единицу измерения

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

Плохо

cooking_time = 30
user_weight = 5

Лучше, но всё ещё плохо:

# Время в минутах
cooking_time = 30

# Вес в килограммах
user_weight = 5

Хорошо

cooking_time_in_minutes = 30
user_weight_in_kg = 5

Дополнительно об этом правиле можно прочитать в книге «Чистый код», глава 2, пункт «Имена должны передавать намерения программиста».

Правило №7 - Названия неиспользуемых переменных заменяются на нижнее подчёркивание

Напишем следующий код:

for i in range(10):
    print("Hello!")

Переменная i внутри цикла не используется. Заменим её на нижнее подчеркивание - традиционное обозначение неиспользуемых переменных:

for _ in range(10):
    print("Hello!")

С точки зрения Python мы поменяли имя переменной i на _. Работа программы от этого не изменилась. Но зато человек, который будет читать код, поймёт, что внутри цикла не используется итерационная переменная.

Это правило обычно применяется и при распаковке последовательностей:

# a = 1; _ = 2
a, _ = 1, 2

# a = 1; _ = [2, 3, 4]
a, *_ = (1, 2, 3, 4)
a, *_ = [1, 2, 3, 4]
a, *_ = {1, 2, 3, 4}
a, *_ = {1: '1', 2: '2', 3: '3', 4: '4'}

Т.е. значения 2, 3 и 4 мы использовать не собираемся, но сохранить их где-то надо.

Когда не следует использовать это правило

Не используйте это правило если пишите на Django, и в вашем коде есть функция gettext. Её принято заменять на нижнее подчёркивание. Хотя ошибки в коде не произойдет, но у программиста может возникнуть недопонимание:

from django.utils.translation import gettext as _


title = _("Интернет-магазин «Кошачий рай»")

# Программист: «Почему здесь исользуется функция gettext?»
for _ in range(10): # Цикл спокойно работает
    print(1)

Дополнительно об этом правиле читайте тут.

Числа

Правило №8 - Число разделяется нижним подчеркиванием через каждые 3 цифры

Для удобства пользователя, в большинстве приложений числа разделяются пробелом через каждые 3 цифры. Например, вместо 1000000 пишется 1 000 000. В Python тоже есть такая возможность, но вместо пробела используется нижнее подчеркивание.

Плохо

number_of_accounts = 1500

sum_in_rubles = 1234567890

Хорошо

number_of_accounts = 1_500

sum_in_rubles = 1_234_567_890

Дополнительно о правиле читайте в этой статье, в пункте «Example 5: Single underscore in numeric literals».

Правило №9 - Число пишется в виде формулы, если его можно так записать

Плюсы применения правила:

  1. Легче и быстрее понять, как появилось число;

  2. Легче и быстрее изменить число - надо просто поменять параметры формулы;

  3. Из кода удаляются «магические числа»;

  4. В коде становится меньше лишних комментариев.

Плохо:

flight_time_in_seconds = 10_800

Лучше, но всё ещё плохо:

# 60 секунд * 60 минут * 3
flight_time_in_seconds = 10_800

Хорошо:

flight_time_in_seconds = 60 * 60 * 3

Очень хорошо:

MIN_IN_SECONDS = 60
HOUR_IN_SECONDS = MIN_IN_SECONDS * 60

flight_time_in_seconds = HOUR_IN_SECONDS * 3

Идеально:

# Код файла constants.py
MIN_IN_SECONDS = 60
HOUR_IN_SECONDS = MIN_IN_SECONDS * 60

# Код файла script.py
from constants import HOUR_IN_SECONDS

flight_time_in_seconds = HOUR_IN_SECONDS * 3

Объём кода становится больше, но времени на осознание и, при необходимости, изменение переменной flight_time_in_seconds - меньше.

Ещё 2 статьи по правилам чистого кода

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

Надеюсь, полученная информация принесла вам пользу. До скорых встреч)

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Узнали для себя новое?
49.4% Да, немного 41
16.87% Да, довольно много 14
33.73% Нет 28
Проголосовали 83 пользователя. Воздержались 5 пользователей.