500 lines or less 是一系列非常經(jīng)典而相對短小的python文章,每一章代碼不超過500行,卻實現(xiàn)了一些強大的功能,由業(yè)內(nèi)大牛執(zhí)筆,有很大的學習價值。適合新手了解基本概念,也適合用來python進階。
本篇原文
源碼
其他的一些開源的翻譯文章
引入
某些編程任務中,邏輯很少但是文本內(nèi)容很多。對于這些任務,我們希望有一個更好的工具來解決這些文字為主的問題。模板引擎就是這樣一個工具。在這篇文章中,我們建立了一個簡單的模板引擎。
Web應用程序是以文字為主的任務的最常見例子。在任何Web應用程序的一個重要階段就是生成HTML送達至瀏覽器。只有很少的HTML頁面是純靜態(tài)的,它們基本上至少含有一小點動態(tài)數(shù)據(jù),例如用戶名。通常它們含有更多的動態(tài)數(shù)據(jù):產(chǎn)品列表,朋友的新消息等等。
同時,每個HTML頁面含有大片靜態(tài)文本。并且這些頁面都很龐大,包含文本的字節(jié)數(shù)以萬計。那么,Web應用程序開發(fā)者面臨一個問題:怎樣生成一個靜態(tài)和動態(tài)數(shù)據(jù)混合的大型字符串是最好的?此外,靜態(tài)文本內(nèi)容實際上是HTML標記語言,由團隊中的其他成員——前端設計師創(chuàng)造,這種生成方式最好是他們熟悉的。
為了說明,我們假設要生成這種極簡的HTML:
<p>Welcome, Charlie!</p>
<p>Products:</p>
<ul>
<li>Apple: $1.00</li>
<li>Fig: $1.50</li>
<li>Pomegranate: $3.25</li>
</ul>
在這里,用戶名將是動態(tài)的,產(chǎn)品的名稱和價格也將是動態(tài)的。甚至產(chǎn)品的數(shù)量也是不固定的,因為庫存是變動的。
生成HTML的一種方式是在我們的代碼中增加字符串常量,再將它們和動態(tài)數(shù)據(jù)結合在一起來產(chǎn)生頁面 。動態(tài)數(shù)據(jù)將以某種字符串替換的形式插入。我們的某些動態(tài)數(shù)據(jù)的展現(xiàn)形式是重復的,比如我們的產(chǎn)品列表,這意味著我們有一批重復的HTML塊。所以我們將它單獨處理再與其它部分組合。
以上述方式生成頁面將是這樣的:
# The main HTML for the whole page.
PAGE_HTML = """
<p>Welcome, {name}!</p>
<p>Products:</p>
<ul>
{products}
</ul>
"""
# The HTML for each product displayed.
PRODUCT_HTML = "<li>{prodname}: {price}</li>\n"
def make_page(username, products):
product_html = ""
for prodname, price in products:
product_html += PRODUCT_HTML.format(
prodname=prodname, price=format_price(price))
html = PAGE_HTML.format(name=username, products=product_html)
return html
它能工作,但是給我們增加了很多麻煩。HTML代碼在多個字符串常量里,嵌入在應用代碼中。頁面的邏輯很不清晰,因為靜態(tài)內(nèi)容被分成了幾片。數(shù)據(jù)如何被格式化的細節(jié)丟失在python代碼中。為了修改HTML頁面,我們的前端工程師還得學會修改python代碼。倘若頁面十倍或百倍的復雜,這種方式就讓人手足無措了。
模板
使用模板來生成HTML頁面是一種更好的方式。HTML頁面被創(chuàng)作為一個模板,意味著該文件主要還是靜態(tài)HTML,同時伴有使用特殊符號表示的動態(tài)數(shù)據(jù)片段嵌入其中。上文的極簡頁面變成模板是這樣的:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
此時重點就放在HTML文本上了,只有一點邏輯結構嵌入。將這個文本中心的方法與之前的邏輯中心的代碼相比。我們之前的程序主要是python代碼,有一些HTML代碼嵌入python邏輯中。而這里,大部分都是靜態(tài)HTML標記語言。
模板的多靜態(tài)風格與大多數(shù)編程語言的工作方式恰好相反。比如python的大多數(shù)源文件都是可執(zhí)行代碼,如果你需要一個文字式的靜態(tài)文本,你把它嵌入在一個字符串中:
def hello():
print("Hello, world!")
hello()
當python讀到這個源文件時,它翻譯類似于def hello():這樣的語句為一個要被執(zhí)行的指令。而在print("Hello, world!")中雙引號表明其中的文本只是字面上的意思。這是大多數(shù)編程語言工作的方式:動態(tài)為主,同時有少量靜態(tài)的片段嵌入在指令中。靜態(tài)部分由引號標記。
模板語言恰好相反:它大多是靜態(tài)文字文本,同時用特殊的符號表示可執(zhí)行的動態(tài)部分。
<p>Welcome, {{user_name}}!</p>
這里的文本在生成的HTML頁面中就以字面出現(xiàn)。直到{{}}表示里面的內(nèi)容為動態(tài)模式,里面的變量將在輸出中被替換。
諸如python的"foo = {foo}!".format(foo=17)這樣的字符串格式化函數(shù)是一種小語言的典型例子,這種語言被用來從字符串字面量和要被插入的數(shù)據(jù)創(chuàng)建文本。模板拓展了這個想法,包含了條件和循環(huán)結構,不同之處只是拓展的程度。
這些文件之所以被稱為模板是因為它們被用來產(chǎn)生許多具有相似結構與不同細節(jié)的頁面。
為了在我們的程序中使用HTML模板, 我們需要一個模板引擎:一個接收參數(shù)為一個靜態(tài)模板(包含結構和頁面的靜態(tài)內(nèi)容)和一個動態(tài)上下文(提供嵌入模板的動態(tài)數(shù)據(jù))的函數(shù)。這個模板引擎結合了模板和上下文來生成一個純HTML的字符串。模板引擎的任務是翻譯模板,用動態(tài)數(shù)據(jù)替換其中的動態(tài)片段。
順便一提,模板引擎并不是針對HTML,它能用來產(chǎn)生任何文本結果。比如,它們也用來生成純文本電子郵件消息。但是它們通常用于HTML,偶爾也有一些HTML的特定功能,比如escaping(換碼),這使得它能過向HTML中插入值而不擔心其中是否有HTML中的特殊字符。
支持的語法
模板引擎支持的語法不同。我們的模板語法基于Django,一個流行的Web框架。既然我們用python來實現(xiàn)我們的模板引擎,一些python的概念會出現(xiàn)在我們的語法中。我們已經(jīng)在本章頂部的極簡模板中看到一部分語法,下面是我們將實現(xiàn)的語法的快速摘要。
上下文中的數(shù)據(jù)使用雙大括號插入:
<p>Welcome, {{user_name}}!</p>
當模板被渲染時,模板中的可用的數(shù)據(jù)由上下文提供。后來更多。
模板引擎通常使用簡化的和寬松的語法來提供數(shù)據(jù)中元素的訪問。在python中,這些表述具有不同的效果:
dict["key"]
obj.attr
obj.method()
在我們的模板語法中,所有這些操作都被用點表示:
dict.key
obj.attr
obj.method
圓點將訪問對象的屬性或者字典的值,并且如果結果值是可調用的,它將被自動調用。這與python代碼不同,在python中這些操作具有不同的語法。這導致了簡單的模板語法:
<p>The price is: {{product.price}}, with a {{product.discount}}% discount.</p>
你還可以使用被稱作過濾器的函數(shù)來修改值。過濾器通過一個豎線(管道符)來調用:
<p>Short name: {{story.subject|slugify|lower}}</p>
建立一個有趣的網(wǎng)站通常需要至少一點決策,條件語句要是可用的:
{% if user.is_logged_in %}
<p>Welcome, {{ user.name }}!</p>
{% endif %}
循環(huán)讓我們在頁面中包含數(shù)據(jù)集合:
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}: {{ product.price|format_price }}</li>
{% endfor %}
</ul>
正如其他編程語言,條件和循環(huán)語句可以嵌套來構復雜的邏輯結構。
最后,讓我們可以為模板添加文檔,注釋出現(xiàn)在大括號和井號之間:
{# This is the best template ever! #}
實現(xiàn)方法
模板引擎具有兩個主要的階段:解析模板,然后渲染模板。
渲染模板具體包括:
- 管理動態(tài)上下文和數(shù)據(jù)源
- 執(zhí)行邏輯元素
- 實現(xiàn)點訪問和過濾器執(zhí)行
從解析階段向渲染階段傳遞什么東西是問題的關鍵。解析生產(chǎn)出什么來供渲染?有兩個主要的選擇,我們叫它們解釋和編譯,使用了和其他語言實現(xiàn)相關的術語。
在一個解釋模型中,解析產(chǎn)生一個數(shù)據(jù)結構表示模板的結構。渲染階段遍歷那個數(shù)據(jù)結構,基于找到的指令裝配結果文本。一個真實的例子是Django模板引擎使用這種方法。
在一個編譯模型中,解析產(chǎn)生某種形式的可直接執(zhí)行的代碼。渲染階段執(zhí)行那個代碼,產(chǎn)生結果。Jinja2和Mako都是使用編譯方法的模板引擎。
我們實現(xiàn)的引擎使用編譯方法:我們將模板編譯為python代碼。執(zhí)行時,代碼將結果組裝起來。這里描述的模板引擎一開始是作為coverage.py的一部分寫的,來生成HTML報告。在coverage.py中,只有很少的模板,它們被反復利用產(chǎn)生很多文件??傮w而言,如果模板被編譯為python代碼,程序運行速度更快,因為即使編譯過程比較復雜,它也只需要運行一次,而被編譯的代碼執(zhí)行了很多次,要比解釋一個數(shù)據(jù)結構很多次快很多。
將模板編譯為python代碼有點復雜,但是沒有你想的那么糟糕。此外,編寫能夠寫代碼的程序比編寫程序本身有趣多了!我們的模板編譯器是一個代碼生成的通用技術的小例子。代碼生成技術構成許多強大而靈活的工具的基礎,包括編程語言編譯器。代碼生成可以變得很復雜,但它是一個很值得擁有的有用的技術。
如果模板每次只會被使用很少次,這樣的模板應用可能傾向于解釋方法。編譯模板為python代碼的代價從長遠看有些打了,整體看來,一個更簡單的解釋過程可能會更好。
編譯到Python
在得到模板引擎的代碼之前,我們先看看要它生成的代碼。解析階段將一個模板轉換為一個Python函數(shù)。再次使用我們的小模板:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
我們的引擎將編譯這個模板為python代碼。python代碼的結果看上去不同尋常,因為我們選擇了一些快捷方式來產(chǎn)生輕量級和更快的代碼。下面的python代碼為了可讀性重新輕微的格式化了:
def render_function(context, do_dots):
c_user_name = context['user_name']
c_product_list = context['product_list']
c_format_price = context['format_price']
result = []
append_result = result.append
extend_result = result.extend
to_str = str
extend_result([
'<p>Welcome, ',
to_str(c_user_name),
'!</p>\n<p>Products:</p>\n<ul>\n'
])
for c_product in c_product_list:
extend_result([
'\n <li>',
to_str(do_dots(c_product, 'name')),
':\n ',
to_str(c_format_price(do_dots(c_product, 'price'))),
'</li>\n'
])
append_result('\n</ul>\n')
return ''.join(result)
每個模板都被轉換為一個render_function函數(shù),其接受一個叫做context的數(shù)據(jù)字典。函數(shù)體先解包上下文字典中的數(shù)據(jù)到本地變量,因為對于數(shù)據(jù)的重復使用這樣會快些。所有的上下文數(shù)據(jù)以加上c_前綴的形式變?yōu)楸镜刈兞窟@樣我們可以自由使用本地變量名而不用擔心命名沖突。
模板的結果將是一個字符串。從部件構建一個字符串最快的方式就是創(chuàng)建一個字符串列表,然后將它們組合在一起。result就是一個字符串列表。因為我們將添加字符串到這個列表中,我們捕捉了它的append和extend方法賦給本地變量result_append和result_extend。最后一個創(chuàng)建的本地變量是一個內(nèi)置方法str的速記--to_str。
這些形式的快捷鍵并不尋常。讓我們看得更仔細些:在python中,一個被對象調用的方法如result.append("hello")分兩步執(zhí)行。首先,append屬性從result對象中獲取,然后取得的值被作為函數(shù)調用,傳遞參數(shù)“hello”給它。盡管我們習慣于看到這些步驟被一起執(zhí)行,實際上它們是分開的。如果你儲存了第一步的結果,那么你將在儲存的結果上執(zhí)行第二步。所以下面這兩個代碼片段做的是同樣的事:
# The way we're used to seeing it:
result.append("hello")
# But this works the same:
append_result = result.append
append_result("hello")
在模板引擎代碼中我們用這種分離的方式是的我們不論做多少次第二步,只用做一次第一步。這節(jié)省了我們少量的時間,因為避免了再花時間去查找對象的append屬性。
這是一個微型優(yōu)化的例子:一個不同尋常的編碼技術使我們獲得速度上的微小改進。微型優(yōu)化可能會使代碼變得可讀性差或更令人困惑,所以只對于那些被證明是性能瓶頸的代碼使用才是合理的。開發(fā)者對于怎樣的微型優(yōu)化是合理的存在分歧,而一些新手會過度使用它。這里的優(yōu)化只在時間測試表明它們提升了性能的情況下被加上,即使是一點點的提升。微優(yōu)化具有啟發(fā)性,因為它們使用了python的某些奇異的方面,但是別在你自己的代碼中過度使用它。
str的快捷方式同樣是一個微優(yōu)化。在python中變量可以是函數(shù)本地的或者模塊全局的或者是python內(nèi)置的。查找一個本地變量名的速度要比查找一個全局或內(nèi)置的名稱快。我們習慣于str是一個總是可獲得的內(nèi)置函數(shù),但是python仍然不得不在每次使用它時查找變量名。將它放在一個本地變量中又為我們節(jié)省了一小塊的時間,因為本地的要比內(nèi)建的快。
一旦這些快捷鍵被定義,下面就是考慮從我們的特定模板中生成的python代碼。字符串將被使用append_result或者extend_result快捷鍵添加到result列表,選擇前一個還是后一個取決于我們只有一個字符串要添加還是多個。模板中的文本變成了一個簡單的字符串。
同時具有append和extend方法增加了復雜性,但請記住我們的目的是模板的最快執(zhí)行。對一個項目使用extend意味著要創(chuàng)建該項目的新列表這樣我們才能將它傳遞給extend。
在{{...}}中的表達式將被計算,轉換為字符串,并被添加到result。表達式中的點將被傳入渲染函數(shù)的do_dots函數(shù)處理,以為加點的表達式的意義取決于context中的數(shù)據(jù)形式:它可能是屬性訪問、子項目獲取或者是一個調用。
{% if ... %}和{% for ... %}的邏輯結構都轉換為python的條件語句和循環(huán)。在{% if/for ... %}標簽中的表達式將會變成if/for語句中的表達式,然后直到{% end... %}標簽之前的內(nèi)容都會變成語句的主體。