選擇一個 Python Web 框架:Django vs Flask vs Pyramid
Pyramid, Django, 和 Flask都是優秀的框架,為項目選擇其中的哪一個都是傷腦筋的事。我們將會用三種框架實現相同功能的應用來更容易的對比三者。也可以直接跳到框架實戰(Frameworks in Action)章節查看代碼(code)。
1 簡介
世界上可選的基于Python的web框架有很多。Django, Flask, Pyramid, Tornado, Bottle, Diesel, Pecan, Falcon等等,都在爭取開發者支持。作為一開發者從一堆選擇中篩選出一個來完成項目將會成為下一個大工程。我們今天專注于Flask, Pyramid, 和 Django。它們涵蓋了從小微項目到企業級的web服務。
為了更容易在三者中作出選擇(至少更了解它們),我們將用每一個框架構建同樣的應用并比較它們的代碼,對于每一個方法我們會高亮顯示它的優點和缺點。如果你只想要代碼,直接跳到框架實戰章節(Frameworks in Action),或者查看其在Github上的代碼。
Flask是一個面向簡單需求小型應用的“微框架(microframework)”。Pyramid和Django都是面向大型應用的,但是有不同的拓展性和靈活性。Pyramid的目的是更靈活,能夠讓開發者為項目選擇合適的工具。這意味著開發者能夠選擇數據庫、URL結構、模板類型等等。Django目的是囊括web應用的所有內容,所以開發者只需要打開箱子開始工作,將Django的模塊拉進箱子中。
Django包括一個開箱即用的 ORM ,而Pyramid和 Flask讓開發者自己選擇如何或者是否存儲他們的數據。到目前為止對于非Django的web應用來說最流行的ORM是SQLAlchemy,同時還有多種其他選擇,從 DynamoDB和MongoDB 到簡單本地存儲的LevelDB 或樸實的SQLite。Pyramid被設計為刻使用任何數據持久層,甚至還沒有開發出來的。
2、關于框架
Django的"batteries included" 特性讓開發者不需要提前為他們的應用程序基礎設施做決定,因為他們知道Python已經深入到了web應用當中。Django已經內建了模板、表單、路由、認證、基本數據庫管理等等。比較起來,Pyramid包括路由和認證,但是模板和數據庫管理需要額外的庫。
前面為 Flask和Pyramid apps選擇組件的額外工作給那些使用案例不適用標準ORM的開發者提供了更多的靈活性,同樣也給使用不同工作流和模版化系統的開發者們帶來了靈活性。
Flask,作為三個框架里面最稚氣的一個,開始于2010年年中。Pyramid框架是從Pylons項目開始的,在2010年底獲得 Pyramid這個名字,雖然在2005年就已經發布了第一個版本。Django 2006年發布了第一個版本,就在Pylons項目(最后叫Pyramid)開始之后。Pyramid和Django都是非常成熟的框架,積累了眾多插件和擴展以滿足難以置信的巨大需求。
雖然Flask歷史相對更短,但它能夠學習之前出現的框架并且把注意力放在了微小項目上。它大多數情況被使用在一些只有一兩個功能的小型項目上。例如 httpbin,一個簡單的(但很強大的)調試和測試HTTP庫的項目。
3. 社區
最具活力的社區當屬Django,其有80,000個StackOverflow問題和一系列來自開發者和優秀用戶的良好的博客。Flask和Pyramid社區并沒有那么大,但它們的社區在郵件列表和IRC上相當活躍。StackOverflow上僅有5,000個相關的標簽,Flask比Django小了15倍。在Github上,它們的star近乎相當,Django有11,300個,Flask有10,900個。
三個框架都使用的是BSD衍生的協議。Flask和Django的協議是BSD 3條款,Pyramid的Repoze Public License RPL是BSD協議 4條款的衍生。
4. Bootstrapping
Django和Pyramid都內建bootstrapping工具。Flask沒有包含類似的工具,因為Flask的目標用戶不是那種試圖構建大型MVC應用的人。
4.1 Flask
Flask的hello world應用非常的簡單,僅僅單個Python文件的7行代碼就夠了。
# 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()</pre>
這是Flask沒有bootstrapping工具的原因:沒有它們的需求。從Flask主頁上的Hello World特性看,沒有構建Python web應用經驗的開發者可以立即開始hacking。
對于各部分需要更多分離的項目,Flask有blueprints。例如,你可以將所有用戶相關的函數放在users.py中,將銷售相關的函數放在ecommerce.py中,然后在site.py中添加引用它們來結構化你的Flask應用。我們不會深入這個功能,因為它超出了我們展示demo應用的需求。
4.2 Pyramid
Pyramid 的 bootstrapping工具叫 pcreate,是Pyramid的組成部分. 之前的 Paste 工具套裝提供了 bootstrapping ,但是從那之后被 Pyramid專用工具鏈替代了。
$ pcreate -s starter hello_pyramid # Just make a Pyramid projectPyramid 比 Flask 適用于更大更復雜的應用程序. 因為這一點,它的 bootstrapping工具創建更大的項目骨架. Pyramid 同樣加入了基本的配置文件,一個例子模版和用于將程序打包上傳到 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作為最后描述的框架,Pyramid的bootstrapper非常靈活. 不局限于一個默認的程序;pcreate 可以使用任意數量的項目模版. 包括我們上面用到的pcreate里面的"starter"的模版, 還有 SQLAlchemy- ,ZODB-支持scaffold項目. 在 PyPi可以發現已經為Google App Engine, jQuery Mobile, Jinja2 templating, modern frontend frameworks做好的scaffolds, 還有更多~
4.3 Django
Django 也有自己的 bootstrap 工具, 內置在 django-admin 中.
django-admin startproject hello_django django-admin startapp howdy # make an application within our projectDjango 跟 Pyramid 區別在于: Django 由多個應用程序組成一個項目, 而 Pyramid 以及 Flask 項目是包含 View 和 Model 單一應用程序 . 理論上, Flask 和 Pyramid 的項目允許存在多個 project/app, 不過在默認配置中只能有一個.
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.pyDjango 默認只在項目中創建 空白的 model 和模板文件, 供新手參考的示范代碼不多. 此外, 開發者在發布應用程序的時候, 還要自己配置, 這也是個麻煩.
bootstrap 工具的缺點是沒有指導開發者如何打包應用. 對于那些沒有經驗的新手來說, 第一次部署應用將是個很頭疼的問題. 像 django-oscar 這樣的大社區, 項目都是打包好了, 放在 PyPi 上供大家安裝. 但是 Github 上面的小項目缺少統一的打包方式.
5 模板
一個Python應用能夠響應HTTP請求將是一個偉大的開端,但是有可能你的大多數用戶是沒有興趣使用curl與你的web應用交互的。幸運的是,這三個競爭者提供了使用自定義信息填充HTML的方法,以便讓大伙們能夠享受時髦的Bootstrap 前端。
模板讓你能夠直接向頁面注入動態信息,而不是采用AJAX。你只需要一次請求就可以獲取整個頁面以及所有的動態數據,這對用戶體驗來說是很好的。這對于手機網站來說尤其重要,因為一次請求花費的時間會更長。
所有的模板選項依賴于“上下文環境(context)”,其為模板轉換為HTML提供了動態信息。模板的最簡單的例子是填充已登錄用戶的名字以正確的迎接他們。也可以用AJAX獲取這種動態信息,但是用一整個調用來填寫用戶的名字有點過頭了,而同時模板又是這么的簡單。
5.1 Django
我們使用的例子正如寫的那么簡單,假設我們有一個包含了用戶名的funllname屬性的user對象。在Python中我們這樣向模板中傳遞當前用戶:
def a_view(request): # get the logged in user # ... do more things return render_to_response( "view.html", {"user": cur_user} )擁有這個模板的上下文很簡單,傳入一個Python對象的字典和模板使用的數據結構。現在我們需要在頁面上渲染他們的名字,以防頁面忘了他們是誰。
<!-- 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聲明是為了防止那些不是用戶的情況。匿名用戶不應該在頁面頭部看到“你已經登錄”的字樣。
在if塊內,你可以看到,包含名字非常的簡單,只要用{{}}包含著我們要插入的屬性就可以了。{{是用來向模板插入真實值的,如{{ user.fullname }}。
模板的另一個常用情況是展示一組物品,如一個電子商務網站的存貨清單頁面。
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可以僅僅使用很少的結構來完成目標,因此很容易上手。
5.2 Flask
Flask默認使用受Django啟發的Jinja2模板語言,但也可以配置來使用另一門語言。不應該抱怨一個倉促的程序員分不清Django和Jinja模板。事實是,上面的Django例子在Jinja2也有效。為了不去重復相同的例子,我們來看下Jinja2比Django模板更具表現力的地方。
Jinja和Django模板都提夠了過濾的特性,即傳入的列表會在展示前通過一個函數。一個擁有博文類別屬性的博客,可以利用過濾特性,在一個用逗號分割的列表中展示博文的類別。
<!-- Django --> <div class="categories">Categories: {{ post.categories|join:", " }}</div><!-- now in Jinja --> <div class="categories">Categories: {{ post.categories|join(", ") }}</div></pre>
在Jinja模板語言中,可以向過濾器傳入任意數量的參數,因為Jinja把它看成是 使用括號包含參數的Python函數的一個調用。Django使用冒號來分割過濾器的名字和過濾參數,這限制了參數的數目只能為一。
Jinjia和Django的for循環有點類似。我們來看看他們的不同。在Jinjia2中,for-else-endfor結構能遍歷一個列表,同時也處理了沒有項的情況。
{% 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版的這個功能是一樣的,但是是用for-empty-endfor而不是for-else-endfor。
{% 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通過執行環境和高級特性提供了更多的控制。例如,它可以關閉危險的特性以安全的執行不受信任的模板,或者提前編譯模板以確保它們的合法性。
5.3 Pyramid
與Flask類似,Pyramid支持多種模板語言(包括Jinja2和Mako),但是默認只附帶一個。Pyramid使用Chameleon,一個 ZPT (Zope Page Template) 模板語言的實現。我們來回頭看看第一個例子,添加用戶的名字到網站的頂欄。Python代碼除了明確調用了render_template函數外其他看起來都差不多。
@view_config(renderer='templates/home.pt') def my_view(request): # do stuff... return {'user': user}但是我們的模板看起來有些不同。ZPT是一個基于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(模板屬性語言)提供了基本的條件語句,字符串的格式化,以及填充標簽內容。上面的例子只用了TAL來完成相關工作。對于更多高級任務,就需要TALES和METAL。TALES( 模板屬性表達式語法的語言)提供了像高級字符串格式化,Python表達式評估,以及導入表達式和模板的表達式。
METAL(宏擴展模板屬性語言)是Chameleon模板最強大的(和復雜的)一部分。宏是可擴展的,并能被定義為帶有槽且當宏被調用時可以被填充。
6. 利用框架行動起來
對于各個框架,我們將通過制作一個叫做wut4lunch的應用來了解,這個應用是告訴整個互聯網你午飯吃了什么的社交網絡。很自由的一個起始想法,完全可以隨意改變。應用將有一個簡單的接口,允許用戶提交他們午飯的內容,并看到其他用戶吃的什么的列表。主頁完成后將看起來像這樣。
6.1 使用Flask的Demo應用
最短的實現用了34行Python代碼和一個22行的Jinja模板。首先,我們有些管理類的任務要做,比如初始化我們的應用并拉近我們的ORM。
from flask import FlaskFor 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)</pre>
現在我們看下我們的模型,這將和另兩個樣例基本一樣。
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數據類型,選擇數據庫中String域的長度。使用我們的模型也超級簡單,這在于我們將要看到 SQLAlchemy查詢語法。
構建我們的提交表單也很簡單。在引入Flask-WTForms和正確的域類型后,你可以看到表單看起來有點像我們的模型。主要的區別在于新的提交按鈕和食物與提交者姓名域的提示。
應用中的SECRET_KEY域是被WTForms用來創建CSRF符號的。它也被itsdangerous(Flask內包含)用來設置cookies和其他數據。
from flask.ext.wtf import Form from wtforms.fields import StringField, SubmitFieldapp.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!')</pre>
讓表單在瀏覽器中顯示意味著模板要有它。我們像下面那樣傳遞進去。
from flask import render_template@app.route("/") def root(): lunches = Lunch.query.all() form = LunchForm() return render_template('index.html', form=form, lunches=lunches)</pre>
好了,發生了什么?我們得到已經用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></pre>
這就是模板的真實情況,我們在已經吃過的午餐中循環,并在<ul>中展示他們。這幾乎與我們前面看到的循環例子一樣。
<ul> {% for lunch in lunches %} <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></pre>
模板的<form>部分僅僅渲染我們在root()視圖中傳入模板的WTForm對象的表單標簽和輸入。當表單提交時,它將向/new提交一個POST請求,這個請求會被下面的函數處理。
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'))</pre>
在驗證了表單數據后,我們把內容放入我們Model對象中,并提交到數據庫。一旦我們在數據庫中存了午餐,它將在人們吃過的午餐列表中出現。
if __name__ == "__main__": db.create_all() # make our sqlalchemy tables app.run()最后,我們只需做(非常)少量的工作來讓應用運行起來。使用SQLAlchemy,我們可以創建存儲午餐的表,然后開始運行我們寫的路徑管理就行了。
6.2 測試Django版APP
Django版wut4lunch 和Flask版有點像,但是在Django項目中被分到了好幾個文件中。首先,我們看看最相似的部分:數據庫模型。它和SQLAlchemy版本的唯一不同之處是聲明保存文本的數據庫字段有輕微的語法區別。
# from wut4lunch/models.py from django.db import modelsclass Lunch(models.Model): submitter = models.CharField(max_length=63) food = models.CharField(max_length=255)</pre>
在表單系統上。不像Flask,我們可以用Django內建的表單系統。它看起來非常像我們在Flask中使用的WTFroms模塊,只是語法有點不同。
from django import forms from django.http import HttpResponse from django.shortcuts import render, redirectfrom .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?')</pre>
現在我們只需要構造一個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, } )</pre>
render函數是Django shortcut,以接受請求、模板路徑和一個上下文的dict。與Flask的render_template類似,它也接受接入請求。
def newlunch(request): l = Lunch() l.submitter = request.POST['submitter'] l.food = request.POST['food'] l.save() return redirect('home')保存表單應答到數據庫是不一樣的,Django調用模型的 .save()方法以及處理會話管理而不是用全局數據庫會話。干凈利落!
Django提供了一些優雅的特性,讓我們管理用戶提交的午餐,因此我們可以刪除那些不合適的午餐信息。Flask和Pyramid沒有自動提供這些功能,而在創建一個Django應用時不需要寫另一個管理頁面當然也是其一個特性。開發者的時間可不免費啊!我們所要做的就是告訴Django-admin我們的模型,是在wut5lunch/admin.py中添加兩行。
from wut4lunch.models import Lunch admin.site.register(Lunch)Bam。現在我們可以添加刪除一些條目,而無需額外的工作。
最后,讓我們看下主頁模板的不同之處。
<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標簽可以使你重建應用中的URLs,而不需破壞視圖。這個是因為url標簽會主動查詢視圖中的URL。
<form action="{% url 'newlunch' %}" method="post"> {% csrf_token %} {{ form.as_ul }} <input type="submit" value="I ate this!" /> </form>表單被不同的語法渲染,我們需要人工在表單主體中添加CSRF token,但這些區別更多的是裝飾
6.3測試Pyramid版App
最后,我們看看用Pyramid實現的同樣的程序。與Django和Flask的最大不同是模板。只需要對Jinja2做很小的改動就足以解決我們在Django中的問題。這次不是這樣的,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>與Django模板類似,缺少for-else-endfor結構使得邏輯稍微的更清晰了。這種情況下,我們以if-for 和 if-not-for 語句塊結尾以提供同樣的功能。使用{{或{%來控制結構和條件的Django以及AngularJS類型的模板讓使用XHTML標簽的模板顯得很外行。
Chameleon模板類型的一大好處是你所選擇的編輯器可以正確的使語法高亮,因為模板是有些得XHTML。對于Django和Flask模板來說,你的編輯器需要能夠正確的支持這些模板語言高亮顯示。
<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></pre>
Pyramid中表單得轉換稍微更細致些,因為pytamid_simpleform不像Django表單的form.as_ul函數那樣可以自動轉換所有的表單字段。
現在我們看看什么返回給應用。首先,定義我們需要得表單并呈現我們的主頁。
# 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)}</pre>
獲取午餐的查詢語法和Flask的很相似,這是因為這兩個demo應用使用了流行的SQLAlchemy ORM來提供持久存儲。在Pyramid中,允許你直接返回模板上下文的字典,而不是要調用特殊的render函數。@view_config裝飾器自動將返回的上下文傳入要渲染的模板。避免調用render方法使得Pyramid寫的函數更加容易測試,因為它們返回的數據沒有被模板渲染對象掩蓋。
@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('/')</pre>
從Pyramid的請求對象中更加容易得到表單數據,因為在我們獲取時會自動將表單POST數據解析成dict。為了阻止同一時間多并發的請求數據庫,ZopeTransactions模塊提供了上下文管理器,對寫入邏輯事物的數據庫進行分組,并阻止應用的線程在各個改變時互相影響,這在你的視圖共享一個全局session并接收到大量通信的情況下將會是個問題。
7. 總結
Pyramid是三個中最靈活的。它可以用于小的應用,正如我們所見,但它也支撐著有名的網站如Dropbox。開源社區如Fedora選擇它開發應用,如他們社區中的徽章系統,從項目工具中接受事件的信息,并向用戶獎勵成就類型的徽章。對于Pyramid的一個最常見的抱怨是,它提供了這么多的選項,以至于用它開始一個新項目很嚇人。
目前最流行的框架是Django,使用它的網站列表也令人印象深刻。Bitbucket,Pinterest,Instagram,以及Onion完全或部分使用Django。對于有常見需求的網站,Django是非常理智的選擇,也因此它成為中大型網站應用的流行選擇。
Flask對于那些開發小項目、需要快速制作一個簡單的Python支撐的網站的開發者很有用。它提供小型的統一工具,或者在已有的API上構建的簡單網絡接口。可以快速開發需要簡單web接口并不怎么配置的后端項目使用Flask將會在前端獲益,如jitviewer提供了一個web接口來檢測PyPy just-in-time的編譯日志。
這三個框架都能解決我們簡單的需求,我們已經看到了它們的不同。這些區別不僅僅是裝飾性的,它們將會改變你設計產品的方法,以及添加新特性和修復的速度。因為我們的例子很小,我們看到Flask的閃光點,以及Django在小規模應用上的笨重。Pyramid的靈活并未體現出來,因為我們的要求是一樣的,但在真實場景中,新的需求會常常出現。
7.1 致謝
標題圖像的logo來自與Flask、Django和Pyramid項目網站。
這篇文章非常感謝它的評閱者,Remy DeCausemaker,Ross Delinger和Liam Middlebrook,忍受了許多初期的草稿。
這篇文章的當前樣式來自于Adam Chainz、bendwarn、Serger Maertens、Tom Leo和wichert的評論和修正(名字按字母表順序)。