[Перевод] Улучшение Python-кода: 12 советов для начинающих
В мои обязанности входит наём Python-разработчиков. Если у заинтересовавшего меня специалиста есть GitHub-аккаунт — я туда загляну. Все так делают. Может быть, вы этого и не знаете, но ваш домашний проект, не набравший ни одной GitHub-звезды, может помочь вам в получении работы.
То же самое относится и к тестовым задачам, выдаваемым кандидатам на должность программиста. Как известно, мы, когда впервые видим человека, формируем первое впечатление о нём за 30 секунд. Это влияет на то, как мы будем, в дальнейшем, оценивать этого человека. Мне кажется несправедливым то, что люди, обладающие привлекательной внешностью, добиваются всего легче, чем все остальные. То же самое применимо и к коду. Некто смотрит на чей-то проект и что-то тут же бросается ему в глаза. Ошмётки старого кода в репозитории — это как крошки хлеба, застрявшие в бороде после завтрака. Они могут напрочь испортить первое впечатление. Может, бороды у вас и нет, но, думаю, вам и так всё ясно.
Обычно легко понять то, что некий код написан новичком. В этом материале я дам вам несколько советов о том, как обыграть кадровиков вроде меня и повысить свои шансы на получение приглашения на собеседование. При этом вас не должна мучить мысль о том, что, применяя эти советы, вы кого-то обманываете. Вы не делаете ничего дурного. Применяя те небольшие улучшения кода, о которых пойдёт речь, вы не только повышаете свои шансы на успешное прохождение собеседования, но и растёте как программист. Не могу сказать, что профессиональному росту способствует упор на заучивание алгоритмов или модулей стандартной библиотеки.
В чём разница между новичком и более опытным разработчиком? Новичок не работал с устаревшими кодовыми базами. Поэтому он не видит ценности в том, чтобы вкладывать время в написание кода, который легко поддерживать. Часто новички работают в одиночку. Они, в результате, не особенно заботятся о читабельности кода.
Ваша цель заключается в том, чтобы показать то, что вас заботит читабельность вашего кода и возможность его поддержки в будущем.
Поговорим о том, как повысить качество ваших 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-проектах недочёты, о которых говорит автор этой статьи?