1 前言
Pyramid,Django和Flask都是非常不错的Web框架,如何为你的项目从中选择最合适的是一个问题。本文中,会使用这三个Web框架来实现具备同一个功能的网站,以此来进行对比。
2 简介
PythonWeb框架的世界里总是充满着选择,Django,Flask,Pyramid,Tornado,Bottle,Diesel,Pecan,Falcon和其他各式各样的框架摆在开发者的眼前。作为一个开发者,你需要能够从中选出一款适合你的能帮助你完成项目的框架。本文中,我们把注意力集中到Flask,Pyramid和Django上。
我们将使用这三款框架去构建同样一个应用并且对比代码,体现出每种框架的优势和劣势。如果你想直接看代码,请点击https://github.com/ryansb/wut4lunch_demos
Flask是一个“微型”框架,主要关注一些简单的应用。Pyramid和Django都是关注大型应用的框架,但是使用不同的方法来达到不同的扩展性和灵活性。Pyramid关注灵活性,让开发者为自己的项目自主选择合适的工具。这意味着开发者可以选择数据库、URL架构、模板风格等等。Django注重将一个web应用所需的所有模块都包括进来,开发者只需要直接使用该框架进行工作即可。
Django默认自带ORM,而Pyramid和Flask让开发者自行去选择如何存储数据。非Django的web应用最常使用的ORM是SQLAlchemy。当然还有其他众多的选择,如DynamoDB和MongoDB、LevelDB、SQLite等。
3 关于框架
Django自带所有所需模块的模式使得开发者能够非常容易地直接关注web应用开发本身,而不需要去做很多关于应用架构如何设计的决定。Django内嵌了模板、表单、路由、认证、基本数据库管理等模块。Pyramid包括路由和认证,但是模板和数据库管理则需要额外的库。
Flask,三个框架中最年轻的一个,始于2010年中。Pyramid框架出自Pylons,在2010年末得名,最早的一个版本是在2005年。Django在2006年发布第一个版本,就在Pylons刚开始的时候。Pyramid和Django都是非常成熟的框架,有相当多的插件和扩展模块来满足各类需求。
虽然Flask更年轻一些,但其更有机会去学习之前的框架并把自己的关注点放到了小项目上。其经常被只有一两个功能的小项目使用,比如httpbin,http://httpbin.org/,一个简单但是强力的调试和测试HTTP的库。
4 社区
从*上各个框架相关的问题数量就可以看出哪个框架更加受欢迎了。
5 引导程序
Django和Pyramid都内嵌各自的引导程序。Flask并未内嵌的原因是其使用者并不会使用Flask去开发大型的MVC应用。
5.1 Flask
7行代码就可以组成一个基于Flask的Hello World应用。
- # from http://flask.pocoo.org/ tutorial
- from flask import Flask
- app = Flask(__name__)
- @app.route("/") # take note of this decorator syntax, it's a common pattern
- def hello():
- return "Hello World!"
- if __name__ == "__main__":
- app.run()
这就是Flask没有内嵌引导程序的原因了:没有这样的需求。
基于上面这个例子,这是一个已经可以运行的基于Flask的网站了。因此,甚至一个没有开发过Python Web应用的开发者都可以直接开始开发了。
对于那些不同功能组件 需要分割的项目,Flask有blueprints机制。例如,你可以将所有用户相关的功能放在users.py文件中,所有销售相关的功能放在ecommerce.py中,然后将他们导入到site.py中。这里我们不再举例说明了,超出了这个实例程序讨论的范围。
5.2 Pyramid
Pyramid的引导程序叫做pcreate,是Pyramid的组成部分。
- $ pcreate-s starter hello_pyramid # Just make a Pyramid project
Pyramid 通常被用来开发比Flask更大型的应用。因此,pcreate创建的项目有着更多的内容,包括基本的配置文件、模板示例和需要将你的项目上传到Python Package Index所需要的文件。
- hello_pyramid
- ├── CHANGES.txt
- ├── development.ini
- ├── MANIFEST.in
- ├── production.ini
- ├── hello_pyramid
- │ ├── __init__.py
- │ ├── static
- │ │ ├── pyramid-16x16.png
- │ │ ├── pyramid.png
- │ │ ├── theme.css
- │ │ └── theme.min.css
- │ ├── templates
- │ │ └── mytemplate.pt
- │ ├── tests.py
- │ └── views.py
- ├── README.txt
- └── setup.py
5.3 Django
Django也有自带的引导程序———django-admin。
- django-admin startproject hello_django
- django-admin startapp howdy # make an application within our project
我们已经可以明显地看出Django和Pyramid的不同之处了。Django将不同的项目划分到不同的应用中去。而Flask和Pyramid会多个应用放到一个项目中去,通过不同的view来进行区分。当然,开发者手动地进行区分也是可行的,但是默认并不进行区分。
- hello_django
- ├── hello_django
- │ ├── __init__.py
- │ ├── settings.py
- │ ├── urls.py
- │ └── wsgi.py
- ├── howdy
- │ ├── admin.py
- │ ├── __init__.py
- │ ├── migrations
- │ │ └── __init__.py
- │ ├── models.py
- │ ├── tests.py
- │ └── views.py
- └── manage.py
Django默认只提供空的model和模板文件,新用户只能看到一些简单的示例代码。
引导程序不引导用户去打包他们的应用,这种做法有一个不足之处,是新手没有这个意识去这样做。如果一个开发者之前没有打包过应用,那么他会发现他第一次部署的时候会有多么的困难。但基本上小型的项目都没有统一打包。
6 模板
有了基本的HTTP 服务器还远远不够,用户肯定希望你还能有个风光靓丽的界面。
模板让你能够动态地更改页面信息,而不要调用AJAX。这个机制从用户的角度来说非常的友好,因为你只需要取一次全页面的信息和其他动态的数据,对于一些移动设备访问网站来说,这样的行为能为他们节省许多时间。
所有的模板都基于context,提供动态的信息给模板并最终渲染到HTML中。让我们来看一个例子,将用户的登录名显示给他们看。
6.1 Django
我们的示例非常简单,假设我们有一个user对象,其有一个fullname属性,包含了user的名字。我们会这样传递给模板:
- def a_view(request):
- # get the logged in user
- # ... do more things
- return render_to_response(
- "view.html",
- {"user": cur_user}
- )
接下来我们需要将user对象传入到模板中去。可以在其中直接访问其fullname属性。
- <!-- view.html -->
- <div class="top-bar row">
- <div class="col-md-10">
- <!-- more top bar things go here -->
- </div>
- {% if user %}
- <div class="col-md-2 whoami">
- You are logged in as {{ user.fullname }}
- </div>
- {% endif %}
- </div>
首先,你会发现其中{% if user %}这个结构。在Django中{%是用来放循环或条件控制等语句的。if user用来判断是否有user传入。匿名user是无法看到’You are logged in as xxx’的页面的。
在if代码块中,只需要简单地把对象及其属性放到{{ }}中去即可,模板就会自动去调用实际的值。
另一个模板的常见用法是显示成组的信息,比如商业网站的库存信息:
- def browse_shop(request):
- # get items
- return render_to_response(
- "browse.html",
- {"inventory": all_items}
- )
其中,我们仍然可以使用{%来循环遍历仓库中所有的项目并且填充到各自的页面中,如:
- {% for widget in inventory %}
- <li><a href="/widget/{{ widget.slug }}/">{{ widget.displayname }}</a></li>
- {% endfor %}
对于大部分的需求来说,Django的模板系统可以通过非常简单的结构来满足。
6.2 Flask
Flask默认使用Django推荐的Jinja2模板语言,但是也可以配置成使用其他的语言。实际上Jinja2和Django的模板系统还是有一些不同之处的,Jinja2更加易读。
Jinja2和Django的模板系统偶读提供了filtering功能。一个list在被显示之前可以被一个指定的函数进行处理。例如:
- <!-- Django -->
- <div class="categories">Categories: {{ post.categories|join:", " }}</div>
- <!-- now in Jinja -->
- <div class="categories">Categories: {{ post.categories|join(", ") }}</div>
Jinja2的模板语言中,能够传递任意数量的参数给filter,因为Jinja2进行的是函数的调用,将参数传递给filter之后的函数。Django使用一个冒号作为filter和其参数的分隔符,这导致了参数的数量被限制在了1个,因为只能有一个冒号,冒号后面的内容都被认作为是参数。
Jinja2和Django的模板系统对于for循环都是类似的,让我们来看看他们的区别在哪里。在Jinja2中,附带了for-else-endfor架构,让你能够遍历list,并且在没有内容的情况下进入else进行处理。
- {% for item in inventory %}
- <div class="display-item">{{ item.render() }}</div>
- {% else %}
- <div class="display-warn">
- <h3>No items found</h3>
- <p>Try another search, maybe?</p>
- </div>
- {% endfor %}
Django也有类似的结构,但是关键字是empty。如:
- {% for item in inventory %}
- <div class="display-item">{{ item.render }}</div>
- {% empty %}
- <div class="display-warn">
- <h3>No items found</h3>
- <p>Try another search, maybe?</p>
- </div>
- {% endfor %}
除了上面提到的各类区别,Jinja2提供了更多的控制或功能。比如其安全性和提前编译模板以避免语法错误等功能,使其面对Django更优一筹。
6.3 Pyramid
Pyramid也支持很多模板语言(如Jinja2或Mako),但是其使用Chameleon作为默认的模板系统。我们来看看前面提到的显示username的例子在Pyramid里是如何实现的,Python 代码看上去比较类似:
- @view_config(renderer='templates/home.pt')
- def my_view(request):
- # do stuff...
- return {'user': user}
但是模板的写法就非常不一样了。ZPT(Zope Page Template)是一个基于XML的模板标准,所有我们使用XSLT类似的语法:
- <div class="top-bar row">
- <div class="col-md-10">
- <!-- more top bar things go here -->
- </div>
- <div tal:condition="user"
- tal:content="string:You are logged in as ${user.fullname}"
- class="col-md-2 whoami">
- </div>
- </div>
Chameleon实际上使用三个不同的命名空间来进行模板内的操作。TAL(template attribute language)提供了基本的条件判断、字符串格式化和填充tag内容等操作。上面的例子就是使用了tal来进行填充。对于更高级的用法,我们就需要使用到TALES和METAL。TALES(Template Attribute Language Expression Syntax)提供了高级字符串处理的表达式和Python表达式求值、表达式导入和模板等。
METAL(Macro Expansion Template Attribute Language)是最强大也是最复杂的部分。其是可扩展的。
7 各个框架的实例展示
下面我们来创建一个APP叫做wut4lunch,用来告诉别人你午饭吃了什么。这个APP允许用户上传他们午餐吃了什么,然后浏览别人吃的什么,预期主页面是这个样子的:
7.1 Flask
代码非常简单,主要包括初始化APP和从ORM获取信息。
- from flask import Flask
- # For this example we'll use SQLAlchemy, a popular ORM that supports a # variety of backends including SQLite, MySQL, and PostgreSQL
- from flask.ext.sqlalchemy import SQLAlchemy
- app = Flask(__name__)
- # We'll just use SQLite here so we don't need an external database app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
- db = SQLAlchemy(app)
接下来是model模块,基本上类似
- class Lunch(db.Model):
- """A single lunch"""
- id = db.Column(db.Integer, primary_key=True)
- submitter = db.Column(db.String(63))
- food = db.Column(db.String(255))
是不是很简单?最困难的部分就是找到正确的SQLAlchemy data类型和长度。
构建提交的表单也是很简单的。导入Flask-WTForms和正确的field类型,你就会发现表单看上去非常像我们的model,主要的区别一个新的提交按钮和输入框的命名。
其中SECRET_KEY这个字段被WTForms用来建立CSRF令牌等其他用途。
- from flask.ext.wtf import Form
- from wtforms.fields import StringField, SubmitField
- app.config['SECRET_KEY'] = 'please, tell nobody'
- class LunchForm(Form):
- submitter = StringField(u'Hi, my name is')
- food = StringField(u'and I ate')
- # submit button will read "share my lunch!"
- submit = SubmitField(u'share my lunch!')
接下来在模板中把表单加进去:
- from flask import render_template
- @app.route("/")
- def root():
- lunches = Lunch.query.all()
- form = LunchForm()
- return render_template('index.html', form=form, lunches=lunches)
在这段代码中,我们通过Lunch.query.all()获取到所有已经上传的午餐信息,并且初始化了一个表单供用户上传他们自己的数据。接下来是模板的内容:
首先是开头的介绍:
- <html>
- <title>Wut 4 Lunch</title>
- <b>What are people eating?</b>
- <p>Wut4Lunch is the latest social network where you can tell all your friends about your noontime repast!</p>
接着是最重要的部分,将我们查询的结果通过循环遍历出来,全部呈现到页面上:
- <li><strong>{{ lunch.submitter|safe }}</strong> just ate <strong>{{ lunch.food|safe }}</strong>
- {% else %}
- <li><em>Nobody has eaten lunch, you must all be starving!</em></li>
- {% endfor %}
- </ul>
- <b>What are YOU eating?</b>
- <form method="POST" action="/new">
- {{ form.hidden_tag() }}
- {{ form.submitter.label }} {{ form.submitter(size=40) }}
- <br/>
- {{ form.food.label }} {{ form.food(size=50) }}
- <br/>
- {{ form.submit }}
- </form>
- </html>
<form>中明显可以看出,当用户POST一个表单的时候,会转入/new指明的模块进行处理:
- from flask import url_for, redirect
- @app.route(u'/new', methods=[u'POST'])
- def newlunch():
- form = LunchForm()
- if form.validate_on_submit():
- lunch = Lunch()
- form.populate_obj(lunch)
- db.session.add(lunch)
- db.session.commit()
- return redirect(url_for('root'))
确认提交上来的数据正确无误之后,就会将其存储到数据库中。以供后续操作查阅。
- if __name__ == "__main__":
- db.create_all()
- # make our sqlalchemy tables
- app.run()
7.2 Django
Django的版本和Flask版本非常类似,但是其分散在不同的文件中。让我们先看最类似的部分:数据库的model。
- # from wut4lunch/models.py
- from django.db import models
- class Lunch(models.Model):
- submitter = models.CharField(max_length=63)
- food = models.CharField(max_length=255)
对于表单系统,Django有自己的一套,和Flask的比起来,只是语法上略有不同。
- from django import forms
- from django.http import HttpResponse
- from django.shortcuts import render, redirect
- from .models import Lunch
- # Create your views here.
- class LunchForm(forms.Form):
- """Form object. Looks a lot like the WTForms Flask example"""
- submitter = forms.CharField(label='Your name')
- food = forms.CharField(label='What did you eat?')
接下来我们需要创建一个LunchForm的实例并将其传递给模板。
- lunch_form = LunchForm(auto_id=False)
- def index(request):
- lunches = Lunch.objects.all()
- return render(
- request,
- 'wut4lunch/index.html',
- {
- 'lunches': lunches,
- 'form': lunch_form,
- }
- )
render函数是Django中用来处理request、模板路径和内容的,类似于Flask中的render_template。
- def newlunch(request):
- l = Lunch()
- l.submitter = request.POST['submitter']
- l.food = request.POST['food']
- l.save()
- return redirect('home')
将用户上传的数据保存到数据库,Django并未采用直接调用数据库session的方法,而是使用了框架自带的.save()方法。非常的整洁!
我们还需要告诉Django-admin我们的model在哪里:
- from wut4lunch.models import Lunch
- admin.site.register(Lunch)
接下来是我们的页面信息:
- <ul>
- {% for lunch in lunches %}
- <li><strong>{{ lunch.submitter }}</strong> just ate <strong>{{ lunch.food }}</strong></li>
- {% empty %}
- <em>Nobody has eaten lunch, you must all be starving!</em>
- {% endfor %}
- </ul>
值得一提的是,Django有一个非常方便的途径在页面中加入其他的视图。url标签让你能够定义APP的URL而不需要破坏视图。例如:
- <form action="{% url 'newlunch' %}" method="post">
- {% csrf_token %}
- {{ form.as_ul }}
- <input type="submit" value="I ate this!" />
- </form>
然后我们还需要包含{% csrf_token%}来确保CSRF正常。但这些都是小问题。
7.3 Pyramid
最后让我们看一看同样的功能,用Pyramid是如何实现的。和前两个最大的区别在于模板。Chameleon模板的语法严格遵循XSLT。
- <!-- pyramid_wut4lunch/templates/index.pt -->
- <div tal:condition="lunches">
- <ul>
- <div tal:repeat="lunch lunches" tal:omit-tag="">
- <li tal:content="string:${lunch.submitter} just ate ${lunch.food}"/>
- </div>
- </ul>
- </div>
- <div tal:condition="not:lunches">
- <em>Nobody has eaten lunch, you must all be starving!</em>
- </div>
表单是这样的:
- <pre name="code" class="html"><b>What are YOU eating?</b>
- <form method="POST" action="/newlunch">
- Name: ${form.text("submitter", size=40)}
- <br/>
- What did you eat? ${form.text("food", size=40)}
- <br/>
- <input type="submit" value="I ate this!" />
- </form>
- </html>
关于表单的渲染也比Django要复杂一些。首先我们需要定义表单,同时渲染主页:
- # pyramid_wut4lunch/views.py
- class LunchSchema(Schema):
- submitter = validators.UnicodeString()
- food = validators.UnicodeString()
- @view_config(route_name='home', renderer='templates/index.pt')
- def home(request):
- lunches = DBSession.query(Lunch).all()
- form = Form(request, schema=LunchSchema())
- return {'lunches': lunches, 'form': FormRenderer(form)}
数据库查询语句的语法和Flask都是类似的,因为两者都用了SQLAlchemy ORM来提供持久性存储。Pyramid中可以直接返回模板的context dictionary,而不用调用render函数。@view_config装饰器自动将返回的context传递给模板进行渲染。
- <pre name="code" class="python">@view_config(route_name='newlunch', renderer='templates/index.pt', request_method='POST')
- def newlunch(request):
- l = Lunch(
- submitter=request.POST.get('submitter', 'nobody'),
- food=request.POST.get('food', 'nothing'),
- )
- with transaction.manager:
- DBSession.add(l)
- raise exc.HTTPSeeOther('/')
表单数据是非常容易可以得到的,因为Pyramid自动将表单数据解析成一个字典供我们调用。ZopeTransactions模块保证了在多用户并发存储的情况下,不同的线程之间不会互相干扰。这也是一个值得一提的地方。
8 总结
Pyramid是三个框架中最灵活的。可以用作类似我们举的例子这种小应用,也可以大到Dropbox这类大型网站。Fedora开源社区选择Pyramid作为他们社区badges system的开发框架。最多的关于Pyramid的抱怨就是使用它的选择太多了,以至于想要新写一个项目必须要先确定好如何去实现。
目前为止最受欢迎的框架是Django,其大型应用例如:Bitbucket、Pinterest、Instagram和Onion。对于大多数需求,Django默认的解决方案都是非常明智的,因此其也非常适用于中到大型的web应用开发。
Flask对于那些希望用Python进行web开发同时开发的项目规模很小功能很简单的开发者而言,是不二的选择。其提供了很多简单的接口和工具供开发者使用,且开发出来的代码量非常小,配置文件的大小也非常小。
这篇文章中提到的区别之处,并不仅仅是表面上看到的那样可能只是些语法上的区别,更多地会影响到整个产品的设计和项目交付的速度。对于我们的示例程序,功能简单,因此Flask就能胜任。Django就会感到笨重。Pyramid的灵活性也没有体现出来。但是在实际的工作中,需求往往是一个接一个来且不断变化的,这时候希望你能想起来到底哪个才是最适合你的!
8.1 鸣谢
感谢很多对这篇文章做出贡献的人!!(具体人名请参照原文!^_^)