[Перевод] Улучшение Python-кода: 12 советов для начинающих

В мои обязанности входит наём Python-разработчиков. Если у заинтересовавшего меня специалиста есть GitHub-аккаунт — я туда загляну. Все так делают. Может быть, вы этого и не знаете, но ваш домашний проект, не набравший ни одной GitHub-звезды, может помочь вам в получении работы.

То же самое относится и к тестовым задачам, выдаваемым кандидатам на должность программиста. Как известно, мы, когда впервые видим человека, формируем первое впечатление о нём за 30 секунд. Это влияет на то, как мы будем, в дальнейшем, оценивать этого человека. Мне кажется несправедливым то, что люди, обладающие привлекательной внешностью, добиваются всего легче, чем все остальные. То же самое применимо и к коду. Некто смотрит на чей-то проект и что-то тут же бросается ему в глаза. Ошмётки старого кода в репозитории — это как крошки хлеба, застрявшие в бороде после завтрака. Они могут напрочь испортить первое впечатление. Может, бороды у вас и нет, но, думаю, вам и так всё ясно.

ulm_ocdnnvfnsmgpcnb6zn_pxxo.jpeg

Обычно легко понять то, что некий код написан новичком. В этом материале я дам вам несколько советов о том, как обыграть кадровиков вроде меня и повысить свои шансы на получение приглашения на собеседование. При этом вас не должна мучить мысль о том, что, применяя эти советы, вы кого-то обманываете. Вы не делаете ничего дурного. Применяя те небольшие улучшения кода, о которых пойдёт речь, вы не только повышаете свои шансы на успешное прохождение собеседования, но и растёте как программист. Не могу сказать, что профессиональному росту способствует упор на заучивание алгоритмов или модулей стандартной библиотеки.

В чём разница между новичком и более опытным разработчиком? Новичок не работал с устаревшими кодовыми базами. Поэтому он не видит ценности в том, чтобы вкладывать время в написание кода, который легко поддерживать. Часто новички работают в одиночку. Они, в результате, не особенно заботятся о читабельности кода.
Ваша цель заключается в том, чтобы показать то, что вас заботит читабельность вашего кода и возможность его поддержки в будущем.

Поговорим о том, как повысить качество ваших Python-проектов. Советы, которыми я хочу поделиться, улучшат ваш код. А если вы не сделаете из них карго-культ, то они ещё и помогут вашему профессиональному росту.

1. Уберите из репозитория ненужные файлы


Откройте страницу своего репозитория на GitHub. Есть ли в нём файлы с расширениями .idea, .vscode, .DS_Store или .pyc? Попали ли туда файлы из виртуального окружения? Если так — избавьтесь от всего этого и добавьте записи о соответствующих файлах и папках в .gitignore. Выкладывая код на GitHub следует придерживаться правила, в соответствии с которым в репозиторий не должно попадать ничего такого, что создано не владельцем репозитория. Вот хорошее руководство по .gitignore, в котором даётся обзор того, что обычно не стоит включать в репозитории.

▍Примеры


Начальный вариант файла .gitignore для Python-проектов


Следующий текст можно рассматривать в качестве начального варианта содержимого .gitignore. Добавьте такой файл в свой проект в самом начале работы над ним.

*.pyc
*.egg-info

# Если вы программируете на Mac
.DS_Store

# Если вы пользуетесь виртуальными окружениями
# в проектах. Я, например, обычно ими пользуюсь.
/env

# Настройки и хранение секретных данных (подробнее об этом - в следующем разделе)
/.env


Если вам нужен более масштабный пример .gitignore — взгляните на этот файл из коллекции GitHub. Используйте его как источник вдохновения и как базу для вашего .gitignore.

2. Не храните в коде секретные данные


В репозитории не должно быть никаких паролей к базам данных, ключей к внешним API, секретных ключей систем шифрования! Подобные вещи надо хранить в конфигурационных файлах или в переменных окружения. Ещё один вариант — их чтение из защищённого хранилища. А включать их в код — это в высшей степени неправильно. Вот — отличное руководство на тему хранения конфигурационных данных, подготовленное в рамках проекта The Twelve-Factor App (другие материалы этого проекта тоже весьма полезны).

▍Примеры


Неправильно: реквизиты базы данных хранятся в коде


Ниже приведён фрагмент Flask-приложения. Автор хранит реквизиты для доступа к базе данных в коде.

from flask import Flask

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://user:secret@localhost:5432/my_db"


Правильно: реквизиты хранятся в переменных окружения


Перенести реквизиты для доступа к базе данных в переменные окружения совсем несложно:

import os
from flask import Flask

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("SQLALCHEMY_DATABASE_URI")


Теперь нужно, перед запуском приложения, инициализировать переменные окружения:

export SQLALCHEMY_DATABASE_URI=postgresql://user:secret@localhost:5432/my_db
flask run


Правильно: реквизиты хранятся в файле .env


Для того чтобы перед запуском программы не приходилось бы вручную инициализировать переменные окружения, можно пойти дальше. А именно, речь идёт о том, чтобы сохранить эти данные в файле .env. Далее, нужно установить пакет python-dotenv и инициализировать переменные окружения прямо из Python-кода.

Вот как может выглядеть файл .env:

SQLALCHEMY_DATABASE_URI=postgresql://user:secret@localhost:5432/my_db


Вот как работать с этим файлом из кода:

import os
from dotenv import load_dotenv
from flask import Flask

load_dotenv()

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("SQLALCHEMY_DATABASE_URI")


И надо не забыть добавить запись об .env в .gitignore. Благодаря этому данный файл не будет случайно выгружен в репозиторий.

3. Добавьте в репозиторий файл README


В проекте, на его верхнем уровне, должен присутствовать файл README, в котором описана цель создания проекта, в котором даются инструкции по установке проекта и по началу работы с ним. Если вы не знаете о том, что писать в таком файле, обратитесь к руководству, размещённому на сайте Make a README.

▍Примеры


Файл README для Python-проекта


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

# Foobar

Foobar is a Python application for dealing with word pluralization.

## Installation

Clone the repository from GitHub. Then create a virtual environment, and install all the dependencies.

```bash
git clone https://github.com/username/foobar.git
python3 -m venv env
source env/bin/activate
python -m pip install -r requirements.txt
```

## Usage

Initialize the virtual environment, and run the script

```bash
source env/bin/activate
./pluralize word
words
./pluralize goos
geese
```

## Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update the tests as appropriate.

## License

[MIT](https://choosealicense.com/licenses/mit/)


4. Если вы используете сторонние библиотеки — добавьте в репозиторий файл requirements.txt


Если в проекте используются сторонние зависимости, об этом нужно сообщить. Легче всего это сделать, создав файл requirements.txt в корневой директории проекта. В каждой строке этого файла приводятся сведения об одной зависимости. Нужно, кроме того, добавить инструкции по работе с этим файлом в README. Подробности о requirements.txt можно найти в руководстве пользователя по pip.

▍Примеры


Файл requirements.txt для Flask-приложения


Добавление файла requirements.txt в корневую директорию проекта — это самый лёгкий способ отслеживания зависимостей. Можно, помимо сведений о самих зависимостях, дать сведения и об их версиях. Вот пример файла requirements.txt:

gunicorn
Flask>=1.1
Flask-SQLAlchemy
psycopg2


Указание более подробных сведений о зависимостях с использованием файла requirements.in


При работе над любым проектом всегда полезно иметь возможность воспроизведения его окружения. В результате, даже если вышла новая версия какой-нибудь библиотеки, можно использовать старую, проверенную в деле версию, работая с ней до тех пор, пока не будет решено перейти на новую. Это называется «фиксацией зависимостей». Легче всего это можно сделать, прибегнув к pip-tools. При таком подходе в вашем распоряжении окажется два файла: requirements.in и requirements.txt. Второй из них при этом вручную не модифицируют, просто добавляя его в репозиторий вместе с requirements.in. Вот как выглядит файл requirements.in:

gunicorn
Flask>=1.1
Flask-SQLAlchemy
psycopg2


Для того чтобы на основе этого файла был бы автоматически создан requirements.txt, файл requirements.in компилируют, используя команду pip-compile. Вот как выглядит автоматически сгенерированный файл requirements.txt:

#
# This file is autogenerated by pip-compile
# To update, run:
#
#    pip-compile
#
click==7.1.2              # via flask
flask-sqlalchemy==2.4.4   # via -r requirements.in
flask==1.1.2              # via -r requirements.in, flask-sqlalchemy
gunicorn==20.0.4          # via -r requirements.in
itsdangerous==1.1.0       # via flask
jinja2==2.11.2            # via flask
markupsafe==1.1.1         # via jinja2
psycopg2==2.8.6           # via -r requirements.in
sqlalchemy==1.3.19        # via flask-sqlalchemy
werkzeug==1.0.1           # via flask

# The following packages are considered to be unsafe in a requirements file:
# setuptools


Как видите, готовый файл содержит сведения о точных версиях всех зависимостей.

5. Форматируйте код с помощью black


Неоднородное форматирование кода не помешает ему нормально работать. Но если код хорошо отформатирован — это улучшит его читабельность и упростит его поддержку. Форматирование кода может и должно быть автоматизировано. Если вы пользуетесь VS Code, то можете увидеть рекомендацию по установке black в качестве автоматического средства форматирования исходного кода, написанного на Python. Форматирование кода производится при сохранении файлов. Кроме того, black можно установить самостоятельно и форматировать код, пользуясь средствами командной строки.

▍Примеры


Неправильно: неформатированный код


Код, приведённый ниже, тяжело читать и расширять.

def pluralize ( word ):
    exceptions={
"goose":'geese','phenomena' : 'phenomenon'   }
    if word in exceptions :
        return exceptions [ word ]
    return word+'s'

if __name__=='__main__' :
    import sys
    print ( pluralize ( sys.argv[1] ) )


Правильно: тот же самый код, отформатированный с помощью black


Применение black гарантирует то, что переформатированный код будет работать так же, как его исходный вариант. Данный инструмент всего лишь снимает с программиста нагрузку по ручному форматированию кода.

def pluralize(word):
    exceptions = {"goose": "geese", "phenomena": "phenomenon"}
    if word in exceptions:
        return exceptions[word]
    return word + "s"

if __name__ == "__main__":
    import sys

    print(pluralize(sys.argv[1]))


6. Избавьтесь от ненужных команд импорта


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

▍Примеры


Неправильно: наличие в коде ненужных команд импорта


В этом фрагменте кода импортированный модуль os не используется:

import os

print("Hello world")


Правильно: в коде нет ненужных команд импорта


Вышеприведённый код очень просто привести в приличный вид:

print("Hello world")


7. Избавьтесь от ненужных переменных


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

▍Примеры


Неправильно: наличие в коде ненужной переменной:


Здесь переменная response не используется:

def ping(word):
    response = requests.get("https://example.com/ping")


Правильно: в коде нет ненужных переменных


Тут нет ничего лишнего:

def ping(word):
    requests.get("https://example.com/ping")


8. Следуйте соглашению по именованию сущностей из PEP 8


Именование сущностей — это как форматирование. Неудачный выбор имён не помешает правильной работе программы, но затруднит работу с кодом. Кроме того, единообразный подход к именованию сущностей снимает с программиста нагрузку, связанную с постоянным выдумыванием имён. Почитать PEP 8 можно здесь.

▍Примеры


Правила именования сущностей из PEP 8


  • Имена файлов и директорий записываются в нижнем регистре с использованием символа подчёркивания для разделения слов: lowercase_underscores.
  • Так же составляют имена функций и переменных: lowercase_underscores.
  • Имена классов записывают с использованием «верблюжьего» стиля: CamelCase.
  • Имена констант записываются в верхнем регистре с использованием символа подчёркивания: UPPERCASE_UNDERSCORE.


Пример применения PEP 8


Ниже приведён фрагмент кода, имеющего достаточно сложную структуру, но соответствующего правилам PEP 8. Тут я, чтобы продемонстрировать именование разных сущностей, поместил простую функцию в класс.

#!/usr/bin/env python
import sys

DEFAULT_NAME = "someone"   # <- UPPERCASE_UNDERSCORE

class GreetingManager:   # <- CamelCase

    def say_hello(self, arguments):  # <- lowercase_underscores
        if len(arguments) < 2:
            target_name = DEFAULT_NAME
        else:
            target_name = arguments[1]   # <- lowercase_underscores
        print(f"Hello, {target_name}")

if __name__ == "__main__":
    GreetingManager().say_hello(sys.argv)


9. Проверяйте код с использованием линтера


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

Различные IDE и редакторы кода, вроде pycharm и VS Code, содержат встроенные линтеры и подсвечивают проблемные участки кода. Программист сам принимает решение о том, следовать этим рекомендациям или нет. Поначалу сообщения об ошибках, выдаваемые линтерами, могут показаться непонятными. Для того чтобы в них ориентироваться, стоит уделить некоторое время изучению используемого линтера. Это себя окупит.

Если говорить о линтерах, представленных инструментами командной строки, то в этой сфере я порекомендовал бы flake8. Этот линтер обладает разумными настройками, применяемыми по умолчанию. Обычно ошибки, о которых он сообщает, стоит исправлять. Если вы хотите строже относиться к своему коду — взгляните на pylint. Этот линтер способен выявлять множество ошибок, в число которых входят и те, о которых мы тут не говорим.

▍Примеры


Файл, который нужно почистить


В нижеприведённом коде (файл ping.py) можно увидеть некоторые проблемы и без применения линтера.

import requests
import os

def PingExample():
    result = requests.get("https://example.com/ping")


Давайте проанализируем его с помощью flake8 и pylint.

Результаты анализа кода с помощью flake8

flake8 ping.py

ping.py:2:1: F401 'os' imported but unused
ping.py:4:1: E302 expected 2 blank lines, found 1
ping.py:5:5: F841 local variable 'result' is assigned to but never used


Результаты анализа кода с помощью pylint

pylint ping.py

************* Module ping
ping.py:1:0: C0114: Missing module docstring (missing-module-docstring)
ping.py:4:0: C0103: Function name "PingExample" doesn't conform to snake_case naming style (invalid-name)
ping.py:4:0: C0116: Missing function or method docstring (missing-function-docstring)
ping.py:5:4: W0612: Unused variable 'result' (unused-variable)
ping.py:2:0: W0611: Unused import os (unused-import)
ping.py:2:0: C0411: standard import "import os" should be placed before "import requests" (wrong-import-order)

--------------------------------------------------------------------
Your code has been rated at -5.00/10 (previous run: -5.00/10, +0.00)


10. Удалите из кода команды print, используемые при отладке


Отладка кода с использованием команд print, расположенных в его важнейших местах, — это нормально. Но не стоит, решив проблему, коммитить в репозиторий код, содержащий подобные команды.

▍Примеры


Неправильно: отладочные команды print в коде


Автор кода захотел узнать о том, к каким изменениям в файловой системе приведёт работа функции, сохраняющей объект в файл. Команды print, выполняемые до и после вызова тела функции, не решают никаких задач, имеющих отношение к самой функции. После того, как они помогли программисту разобраться в происходящем, их нужно удалить. Иначе они будут просто засорять код.

def serialize(obj, filename):
    print("BEFORE", os.listdir())
    with open(filename, "wt") as fd:
        json.dump(obj, fd)
    print("AFTER", os.listdir())


Правильно: код без ненужных команд print


Если убрать из кода отладочные команды — это уменьшит размер функции и повысит удобство работы с ней. А это всегда хорошо.

def serialize(obj, filename):
    with open(filename, "wt") as fd:
        json.dump(obj, fd)


11. Не держите в репозитории закомментированный код


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

▍Примеры


Неправильно: ненужные комментарии в коде


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

name = input("What's your name: ")
#short_name = name.split()[0]
#if len(short_name) > 0:
#    name = short_name
print(f"Hello, {name}")


Правильно: код, в котором нет ненужных комментариев


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

name = input("What's your name: ")
print(f"Hello, {name}")


12. Оформляйте скрипты в виде функций


В самом начале работы над программой её код обычно следует за потоком мыслей программиста. Этот код состоит из последовательности инструкций, решающих некую задачу. Выработайте у себя привычку оформлять последовательности инструкций в виде функций. Поступать так стоит с самого начала работы над проектом. Подобные функции нужно вызывать в самом конце программ, защитившись выражением if __name__ == «__main__». Это поможет вам использовать структурный подход при развитии проекта, извлекая из нужных мест вспомогательные функции. А позже, если надо, это облегчит оформление скриптов в виде модулей.

▍Примеры


Неправильно: скрипт, не оформленный в виде функции


Выполнение кода в этом примере начинается в первой строке и заканчивается в последней. Такой подход оправдан в том случае, если речь идёт о простых скриптах. Но если код скрипта окажется сложнее, воспринять его будет уже не так легко.

#!/usr/bin/env python
name = input("What's your name: ")
print(f"Hello, {name}")


Правильно: скрипт, оформленный в виде функции


Поток выполнения программы начинается в последней строке кода — там, где вызывается функция say_hello(). Если речь идёт о том, что в состав функции входит всего пара строк кода, то такой подход может показаться неоправданно усложнённым. Но это, в любом случае, облегчает изменение кода. Например, можно легко, воспользовавшись click, оснастить свою функцию возможностями по приёму параметров из командной строки.

#!/usr/bin/env python

def say_hello():
    name = input("What's your name: ")
    print(f"Hello, {name}")

if __name__ == "__main__":
    say_hello()


Домашнее задание


Говорят, что мы запоминаем лишь 10% того, что прочли. Это значит, что вы запомните лишь один из 12 данных мной советов. Полагаю, это означает, что вы впустую потратили время, читая эту статью. А я, в таком случае, зря её писал.

Но, к счастью, есть одна хитрость. Известно, что практика позволяет сохранить около 80% знаний. Следовательно — вот вам задание: возьмите один из своих проектов и проанализируйте его с точки зрения моих 12 советов. У того, кто так и сделает, в 8 раз больше шансов профессионально вырасти, чем у того, кто просто прочтёт статью.

Есть ли в ваших Python-проектах недочёты, о которых говорит автор этой статьи?

oug5kh6sjydt9llengsiebnp40w.png

3piw1j3wd_cgmzq9sefgferaumu.png

© Habrahabr.ru