Toly blog

Python and Django notes

Начиная Python-проект: The Right Way

Достаточно вольный (настолько вольный, что отсутствуют два абзаца и изменен код) перевод статьи Джефа Кнаппа

Starting A Python Project The Right Way

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

Установка

До того как написать хоть строчку кода, первое что я делаю - создаю виртуальное окружение. Что такое виртуальное окружение? Это установка python отдельно от остальной части системы (и дефолтного pythona’а). Какая от этого польза? Представьте себе, что у вас есть два проекта, над которыми вы работаете. Если оба испольузют какую-либо библиотеку (например, requests), и в одном из проектов используется старая версия (которую нельзя корректно обновить, т.к. другие библиотеки используют старую версию requests), как вы сможете использовать новую версию requests в другом проекте? С помощью виртуального окружения.

Для начала установите virtualenvwrapper (обертка над фантастическим пакетом virtualenv). Добавьте в ваш .bashrc строчку /usr/local/bin/virtualenvwrapper.sh и перезагрузите свой профиль с помощью source:

source ~/.bashrc

Теперь у вас должна появиться команда mkvirtualenv, доступная через автодополнение с помощью tab. Если вы используете Python старше версии 3.3, виртуальное окружение поддерживает этот язык и установка этого пакета не требуется. mkvirtualenv <my_project> создаст новое виртуальное окружение под названием my_project с уже установленными pip и setuptools. Для Python 3 требуемые команды выглядят так:

python -m venv <my_project>
source <my_project>/bin/activate

Теперь когда виртуальное окружение создано, пришло время инициализировать средство управления исходниками. Предполагая что это git (ну, потому что он…), введем

git init .

Так же полезно добавить в .gitignore все скомпилированые Python-ом файлы и каталоги __pychache__. Для этого создайте файл .gitignore и поместите в него следующее:

*.pyc
__pycache__

Теперь подходящее время добавить в проект README файл. Даже если вы единственный, кто будет видеть код, это хорошее упражнение для организации ваших мыслей. README файл должен описывать что делает проект, его зависимости и как его использовать. Я пишу README файлы с использованием разметки Markdown, во-первых потому что GitHub автоматически оформляет любой файл названный README.md, а во-вторых потому что я пишу все (!) документы в разметке Markdown.

И наконец, сделайте первый коммит содержащий два файла (.gitignore, README.md), которые вы только что создали. Для этого введите:

git add .gitignore README.md
git commit -m "initial commit"

Каркасы!

Почти каждое приложение я начинаю одинаково: создаю каркас приложения, состоящий из функций и классов с заполненой документацией, но без реализации. Я считаю, что необходимо сперва вынужденно писать документацию для функции, иначе если я не способен кратко описать что-либо, то у меня нет достаточно мыслей о проблеме.

В качестве примера приложения я использую скрипт, недавно написанный обучаемым во время одного из наших занятий. Цель скрипта - создать csv-файл, содержащий самые кассовые фильмы прошлого года (по версии IMDB) и ключевые слова связанные с этими фильмами на IMDB. Это был довольно простой проект, для того что бы завершить его за одно занятие, но достаточный по сложности, что бы требовать размышлений.

Сперва создайте основной файл, который будет точкой входа в приложение. Я назвал его imdb.py. Потом скопируйте следующий код в редактор:

1
2
3
4
5
6
7
8
9
""Script to gather IMDB keywords from 2013's top grossing movies."""
import sys

def main():
    """Main entry point for the script."""
    pass

if __name__ == '__main__':
    sys.exit(main())

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"""Script to gather IMDB keywords from 2013's top grossing movies."""
import sys

URL = "http://www.imdb.com/search/title?at=0&sort=boxoffice_gross_us,desc&start=1&year=2013,2013"

def main():
    """Main entry point for the script."""
    pass

def get_top_grossing_movie_links(url):
    """Return a list of tuples containing the top grossing movies of 2013 and link to their IMDB
    page."""
    pass

def get_keywords_for_movie(url):
    """Return a list of keywords associated with *movie*."""
    pass

if __name__ == '__main__':
    sys.exit(main())

Выглядит сносно. Отмечу, что обе функции включают параметры (например, get_keywords_for_movie принимает параметр url). Это может показаться странным для заглушек. Зачем здесь параметры? Аргументация такая же, как и для предварительного документирования заглушек: если я не знаю какие агрументы должна принимать функция, значит я недостаточно об этом думал.

В этом месте я верятно закомичусь, т.к. проделал определенную часть работы, которую не хотел бы потерять. После этого перейдем к реализации. Я всегда начинаю с реализации функции main, т.к. “центр” использующий все остальные функции. Вот реализация функции main в imdb.py:

1
2
3
4
5
6
7
8
9
10
11
import csv

def main():
    """Main entry point for the script."""
    movies = get_top_grossing_movie_links(URL)
    with open('output.csv', 'w') as output:
        csvwriter = csv.writer(output)
        for title, url in movies:
            keywords = get_keywords_for_movie(
                'http://www.imdb.com{}keywords/'.format(url))
            csvwriter.writerow([title, keywords])

Несмотря на то что get_top_grossing_movie_links и get_keywords_for_movie не реализованы, я знаю достаточно о том, как их использовать. Функция main делает именно то, что мы обсуждали вначале: получает самые кассовые фильмы года и пишет их в csv-файл вместе с их ключевыми словами.

Теперь все что осталось, это реализовать недостающие функции. Любопытно, что даже если мы знаем, что get_keywords_for_movie будет вызван после get_top_grossing_movie_links, мы можем реализовать их в том порядке, который больше нравится. Это не тот случай, когда пишешь скрипт с нуля и добавляешь функционал в том порядке, в которм идет разработка. Вы были бы вынуждены полностью написать первую функцию, прежде чем перети ко второй. Тот факт, что мы можем реализовать (и проверить!) функции в любом порядкепоказывает, что они слабо связаны.

Давайте первым реализуем функцию get_keywords_for_movie:

1
2
3
4
5
6
7
8
def get_keywords_for_movie(url):
    """Return a list of keywords associated with *movie*."""
    keywords = []
    response = requests.get(url)
    soup = BeautifulSoup(response.text)
    tables = soup.find_all('table', class_='dataTable')
    table = tables[0]
    return [td.text for tr in table.find_all('tr') for td in tr.find_all('td')]

Мы используем библиотеки requests и BeautifulSoup, поэтому нам нужно установить их через pip. Теперь можно внести в список зависимостей проекта новые библиотеки: pip freeze requirements.txt и закомитить изменения. Таким образом мы всегда сможем создать виртуальное окружение и установить именно те библиотеки (и версии) которые нужны для запуска приложения.

Наконец напишем реализацию для функции get_top_grossing_movie_links:

1
2
3
4
5
6
7
8
9
def get_top_grossing_movie_links(url):
    """Return a list of tuples containing the top grossing movies of 2013 and link to their IMDB
    page."""
    response = requests.get(url)
    movies_list = []
    for each_url in BeautifulSoup(response.text).select('.title a[href*="title"]'):
        movie_title = each_url.text
        movies_list.append((movie_title, each_url['href']))
    return movies_list

Вот финальное содержание imdb.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
""Script to gather IMDB keywords from 2013's top grossing movies."""
import sys
import requests
from bs4 import BeautifulSoup
import csv

URL = "http://www.imdb.com/search/title?at=0&sort=boxoffice_gross_us,desc&start=1&year=2013,2013"

def get_top_grossing_movie_links(url):
    """Return a list of tuples containing the top grossing movies of 2013 and link to their IMDB
    page."""
    response = requests.get(url)
    movies_list = []
    for each_url in BeautifulSoup(response.text).select('.title a[href*="title"]'):
        movie_title = each_url.text
        movies_list.append((movie_title, each_url['href']))
    return movies_list



def get_keywords_for_movie(url):
    """Return a list of keywords associated with *movie*."""
    keywords = []
    response = requests.get(url)
    soup = BeautifulSoup(response.text)
    tables = soup.find_all('table', class_='dataTable')
    table = tables[0]
    return [td.text for tr in table.find_all('tr') for td in tr.find_all('td')]


def main():
    """Main entry point for the script."""
    movies = get_top_grossing_movie_links(URL)
    with open('output.csv', 'w') as output:
        csvwriter = csv.writer(output)
        for title, url in movies:
            keywords = get_keywords_for_movie('http://www.imdb.com{}keywords/'.format(url))
            csvwriter.writerow([title, keywords])


if __name__ == '__main__':
    sys.exit(main())

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

Заключение

Надеюсь теперь у вас есть план действий для начала работы над python-проектом с нуля. Не смотря на то, что у каждого есть свой метод начала работы над проектом, скорее всего мой метод подойдет и вам. Как всегда, если у вас есть какие-либо вопросы, не стесняйтесь задавать их в комментариях или напишите мне на jeff@jeffknupp.com.

Comments