Большинство питонистов не раз слышали о таких правилах как «функции должны быть глаголами» или «не наследуйтесь явно от 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:
-
Позволяет не думать над типом аргумента;
-
Немного документирует код;
-
Уменьшает число ошибок, связанных с типом аргумента;
-
Облегчает разработку в некоторых 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
в таком порядке:
-
Прочитает метод
__init__
и поймёт куда заносится имя"Tom"
; -
Прочитает метод
ask_for_food
и увидит в нём вызов метода__say_meow
; -
Прочитает метод
__say_meow
.
Т.е. приватный пользовательский метод читается в последнюю очередь. Так всегда происходит со всеми не магическими private-методами, если в коде соблюдаются принципы ООП.
Что касается порядка создания публичных и магических методов, то это дело вкуса. Я обычно создаю методы в такой последовательности:
-
__new__
(если такой метод используется в классе); -
__init__
; -
Остальные магические методы;
-
Public-методы;
-
Protected-методы;
-
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 — Число пишется в виде формулы, если его можно так записать
Плюсы применения правила:
-
Легче и быстрее понять, как появилось число;
-
Легче и быстрее изменить число — надо просто поменять параметры формулы;
-
Из кода удаляются «магические числа»;
-
В коде становится меньше лишних комментариев.
Плохо:
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-проектах.
Надеюсь, полученная информация принесла вам пользу. До скорых встреч)
ссылка на оригинал статьи https://habr.com/ru/post/693668/
Добавить комментарий