Skip to content

Instantly share code, notes, and snippets.

@yashaka
Created April 3, 2016 20:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yashaka/56c8630f1316ef9ee8b4582d0b1f2c31 to your computer and use it in GitHub Desktop.
Save yashaka/56c8630f1316ef9ee8b4582d0b1f2c31 to your computer and use it in GitHub Desktop.
Краткое введение в Python для немножко знающих java на примере кода REST веб сервиса на Python + Flask описанного в статье https://habrahabr.ru/post/246699/
#!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