Created
April 3, 2016 20:40
-
-
Save yashaka/56c8630f1316ef9ee8b4582d0b1f2c31 to your computer and use it in GitHub Desktop.
Краткое введение в Python для немножко знающих java на примере кода REST веб сервиса на Python + Flask описанного в статье https://habrahabr.ru/post/246699/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!flask/bin/python | |
# предыдущая строка | |
# - это такой способ в скриптах (не только в пайтон) обозначить что именно это за скрипт:) | |
# ... в нашем случае - пайтон скрипт :) | |
# Предисловие :) | |
# Лучше всего читать этот код параллельно с оригинальной статьей: | |
# Проектирование RESTful API с помощью Python и Flask | |
# на https://habrahabr.ru/post/246699/ | |
# Цель приведенных далее комментариев обьяснить те нюансы Python, | |
# которые нужны для понимания этой статьи. | |
# Здесь нет (или очень мало) комментариев касающихся особенностей Flask. Которых предостаточно в самой статье. | |
# следующие строки - это что-то среднее между import и import static в джава | |
# в пайтон файлы с кодом называются модулями | |
from flask import Flask, jsonify, abort, request, make_response, url_for | |
# можно считать, что здесь из модуля-файла flask.py мы импортим класс Flask | |
# а также импортим "статические методы" jsonify, abort, etc. ... | |
# в пайтон "статические методы" (а также переменные) или другими словами - функции - можно объвялять прямо в файле, | |
# без помещения в класс, как ты увидишь дальше... | |
from flask.ext.httpauth import HTTPBasicAuth | |
# здесь мы типа заимпортили класс HTTPBasicAuth | |
# из файла httpauth.py, живущего в во вложенных папках flask/ext | |
app = Flask(__name__, static_url_path = "") | |
# ~ java: Flask app = new Flask(__name__, "") | |
# как видишь, в пайтон не обязательно объявлять тип у переменной | |
# | |
# также можно, например, для лучшей читабельности (не только для этого) | |
# указывать имя параметра, который мы передаем... | |
# то есть конструктор класса Flask получает два параметра, | |
# и у второго имя - static_url_path, и мы просто для лучшей читабельности | |
# указали это имя в момент передачи аргумента "" | |
# | |
# при этом __name__ - это такая специальная переменная, в которой живет либо то, | |
# что тебе пока будет сложно объяснить :) либо специальное значение "__main__" | |
# в том случае, если этот файл з кодом был запущен как скрипт - а это как раз | |
# наш случай, так что о том первом "либо" тем более можно забыть пока :) | |
auth = HTTPBasicAuth() | |
@auth.get_password # "говорим фласку о том что/аннотируем" следующую функцию как "способ получения пароля по имени пользователя" | |
def get_password(username): # типа `public static String getPassword(String username)` | |
# как видно, тип возвращаемого из функции значения, а также тип параметра, тоже не указываем в пайтон | |
if username == 'miguel': # можно строки записывать и в одинарных кавычках | |
return 'python' | |
return None # типа `return null;` | |
# также как видим - не нужно скобочек {}, просто ставим двоеточие и выделяем отступами блок кода | |
@auth.error_handler | |
def unauthorized(): | |
return make_response(jsonify( { 'error': 'Unauthorized access' } ), 403) | |
# return 403 instead of 401 to prevent browsers from displaying the default auth dialog | |
# штука, записанная выше в фигурных скобках, называется словарем (в джава известна как HashMap) | |
# представляет собой структуру данных в виде пар ключ:значение, записанных через запятую | |
# в нашем случае только одна пара, дальше будет пример с несколькими парами. | |
# и ключи, и значения могут быть любого типа, не обязательно строки... | |
@app.errorhandler(400) | |
def not_found(error): | |
return make_response(jsonify( { 'error': 'Bad request' } ), 400) | |
@app.errorhandler(404) | |
def not_found(error): | |
return make_response(jsonify( { 'error': 'Not found' } ), 404) | |
tasks = [ | |
{ | |
'id': 1, | |
'title': u'Buy groceries', # строка, записанная в скобках, | |
# начинающихся с u - типа как "юникод" строка :) | |
'description': u'Milk, Cheese, Pizza, Fruit, Tylenol', | |
'done': False | |
}, | |
{ | |
'id': 2, | |
'title': u'Learn Python', | |
'description': u'Need to find a good Python tutorial on the web', | |
'done': False | |
} | |
] | |
# здесь видим интересный пример вложенных структур данных | |
# со словарем, записанным в фигурных скобках, уже знакомы | |
# а то, что в квадратных скобках - это список, типа LinkedList<T> в джава, | |
# только тип элементов снова - не важно какой :) | |
# получается, мы положили в переменную tasks список словарей, | |
# каждый из которых представляет собой описание задачи | |
def make_public_task(task): | |
new_task = {} | |
for field in task: # то же, что в джава `for(String field: task){` | |
# правда в джава нельзя было бы так просто | |
# перебрать слова в словаре (хешмапе)... но в пайтон можно :) | |
if field == 'id': | |
new_task['uri'] = url_for('get_task', task_id = task['id'], _external = True) | |
# в джава: | |
# newTask.put("uri", urlFor("get_task", task.get("id"), true); | |
# типа помещаем в словарь newTask новое | |
# значение/перевод urlFor("get_task", task.get("id"), true) | |
# для ключа/"неизвестного слова, которое нужно перевести" "uri" | |
# | |
# где task.get("id") как не сложно догадаться | |
# - это вытащить из словаря перевод/значения | |
# для слова/ключа "id" | |
else: | |
new_task[field] = task[field] | |
return new_task | |
@app.route('/todo/api/v1.0/tasks', methods = ['GET']) | |
# через вот эту "аннотацию" (которые в пайтон называются декораторами кстати) | |
# говорим фласк-серверу, что он должен в ответ на GET запрос клиента | |
# к урлу '/todo/api/v1.0/tasks' | |
# вызвать "аннотированную/декорированную" функцию get_tasks | |
# ну и вернуть клиенту то что она вернет | |
@auth.login_required | |
def get_tasks(): | |
return jsonify( { 'tasks': map(make_public_task, tasks) } ) | |
# вот это интересно, функция map берет функцию make_public_task | |
# (да в пайтон можно имена функций передавать как аргументы) | |
# и применяет ее к каждому элементу списка tasks, | |
# и потом возвращает список "преобразованных/преображенных" значений | |
# простой пример: | |
# имея | |
# def multiply_by_two(x): | |
# return x * 2 | |
# потом вызов | |
# map(multiply_by_two, [1, 2, 3]) | |
# вернет | |
# [1, 4, 6] | |
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods = ['GET']) | |
# кто уже знаком с BDD/Cucumber в джава тот может догадаться | |
# что <int:task_id>' в шаблоне урла позволит в фласку | |
# распарсить инт-овое число в конце строки урла | |
# по которому пришел запрос, и передать это число | |
# в параметр task_id | |
# следующего за "аннотацией" метода get_task | |
@auth.login_required | |
def get_task(task_id): | |
task = filter(lambda t: t['id'] == task_id, tasks) | |
# еще интересней :) | |
# давай начнем с того, что перепишем эту строку в ее более понятный аналог: | |
# def has_needed_task_id(t): | |
# return t['id'] == task_id | |
# task = filter(has_needed_task_id, tasks) | |
# filter работает похоже на map, он тоже применяет функцию | |
# переданную первым аргументом | |
# к каждому элементу списка переданного вторым | |
# только в возвращаемый список результатов пихает не результаты | |
# этого "применения" (как функция map) | |
# а оригинальные же элементы списка, но только в том случае | |
# если результат "применения функции" к этому элементу был True | |
# то есть мы фильтруем список (второй аргумент) по предикату (первому аргументу) | |
# в нашем случае мы отбираем из списка задач только ту задачу | |
# у которой айдишник равен тому, который нам передали в | |
# в нашу функцию get_task - то есть task_id | |
# | |
# еще пример: | |
# имея | |
# def is_positive(x): | |
# return x >= 0 | |
# потом вызов | |
# filter(is_positive, [1, -2, 3]) | |
# вернет | |
# [1, 3] | |
# теперь вопрос, а что за лабуда - `lambda t: t['id'] == task_id` ? | |
# давай вспомним мой пример выше: | |
# def has_needed_task_id(t): | |
# return t['id'] == task_id | |
# task = filter(has_needed_task_id, tasks) | |
# обрати внимание, что мы создаем функцию has_needed_task_id, | |
# которую мы используем только один раз | |
# | |
# вопрос, зачем тогда ее вообще создавать отдельно? правда ведь | |
# было бы классно прямо впихнуть объявление этой | |
# функции прямо в вызов filter, например вот так: | |
# task = filter(def has_needed_task_id(t): return t['id'] == task_id, tasks) | |
# теперь другое наблюдение... а ведь нам все равно какое имя у этой функции, | |
# правда? почему бы его не опустить? | |
# ну вперед: | |
# task = filter(def (t): return t['id'] == task_id, tasks) | |
# еще одно пожелание, так функия маленькая, в одну строку, зачем return писать? | |
# может будем считать что он "автоматом" вызовется? | |
# что, классно ведь: | |
# task = filter(def (t): t['id'] == task_id, tasks) | |
# а теперь вспомни, как было в оригинальном коде: | |
# task = filter(lambda t: t['id'] == task_id, tasks) | |
# получается в пайтон сделали все те улучшения, до которых мы "сами додумались", | |
# только изменили слегка еще синтаксис | |
# вместо `def (t):` пишут `lambda t:` | |
# но смысл тот же - умудриться кратко записать объявление однострочной функции, | |
# для которой не важно имя ;) | |
if len(task) == 0: | |
abort(404) | |
return jsonify( { 'task': make_public_task(task[0]) } ) | |
@app.route('/todo/api/v1.0/tasks', methods = ['POST']) | |
@auth.login_required | |
def create_task(): | |
if not request.json or not 'title' in request.json: # последняя часть - 'title' in request.json: | |
# это что-то типа request.json.contains("title") в джава | |
abort(400) | |
task = { | |
'id': tasks[-1]['id'] + 1, | |
'title': request.json['title'], | |
'description': request.json.get('description', ""), | |
'done': False | |
} | |
tasks.append(task) | |
return jsonify( { 'task': make_public_task(task) } ), 201 | |
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods = ['PUT']) | |
@auth.login_required | |
def update_task(task_id): | |
task = filter(lambda t: t['id'] == task_id, tasks) | |
if len(task) == 0: | |
abort(404) | |
if not request.json: | |
abort(400) | |
if 'title' in request.json and type(request.json['title']) != unicode: | |
abort(400) | |
if 'description' in request.json and type(request.json['description']) is not unicode: | |
abort(400) | |
if 'done' in request.json and type(request.json['done']) is not bool: | |
abort(400) | |
task[0]['title'] = request.json.get('title', task[0]['title']) | |
task[0]['description'] = request.json.get('description', task[0]['description']) | |
task[0]['done'] = request.json.get('done', task[0]['done']) | |
return jsonify( { 'task': make_public_task(task[0]) } ) | |
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods = ['DELETE']) | |
@auth.login_required | |
def delete_task(task_id): | |
task = filter(lambda t: t['id'] == task_id, tasks) | |
if len(task) == 0: | |
abort(404) | |
tasks.remove(task[0]) | |
return jsonify( { 'result': True } ) | |
if __name__ == '__main__': | |
app.run(debug = True) | |
# помнишь, в начале мы уже успели познакомиться что такое __name__ и знаем, | |
# что если этот файл будет запущен как скрипт | |
# с консольки, типа: $ python rest-server-explained.py | |
# то внутри этой специальной переменной будет жить как раз '__main__' | |
# получается, что этот иф позволяет нам в случае запуска | |
# этого файлика с кодом как скрипта | |
# - запустить наш фласк-сервер (app = Flask(__name__, static_url_path = "") | |
# на работу app.run(debug = True) | |
# то есть прослушивание всех запросов от клиента :) | |
# и выдачу ответов в соответствии с нашим REST API | |
# вопрос - а что, нельзя было просто `app.run(debug = True)` написать? | |
# без всяких ифов... ? | |
# - можно... но помнишь, файлики с пайтон кодом - по сути есть модулями... | |
# а значит | |
# где-то в другом файлике кто-то может написать import rest-server-explained | |
# например, чтобы переиспользовать наши функции, какие мы тут понаписывали... | |
# и поскольку при импорте весь код того, что импортируется, будет "выполнен", | |
# то запусится и наш app.run(debug = True) | |
# прямо в момент импорта, и получается, это "заблочит" выполнение того кода, | |
# который хотел то всего несколько наших | |
# функций поиспользовать... :) | |
# и получается, наш иф как раз этого не допустит, он увидит что в __name__ | |
# будет жить не __main__, а что-то другое | |
# (на самом деле, имя того файла с кодом, | |
# в котором осуществляется import rest-server-explained) | |
# и не даст запуститься нашему серверу через app.run(debug = True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment