用Python創(chuàng)建聲明性迷你語(yǔ)言的教程
大多數(shù)程序員考慮編程時(shí),他們都要設(shè)想用于編寫應(yīng)用程序的 命令式樣式和技術(shù)。最受歡迎的通用編程語(yǔ)言(包括 Python 和其它面向?qū)ο蟮恼Z(yǔ)言)在樣式上絕大多數(shù)都是命令式的。另一方面,也有許多編程語(yǔ)言是 聲明性樣式,包括函數(shù)語(yǔ)言和邏輯語(yǔ)言,還包括通用語(yǔ)言和專用語(yǔ)言。
讓我們列出幾個(gè)屬于各個(gè)種類的語(yǔ)言。許多讀者已經(jīng)使用過(guò)這些工具中的許多工具,但不見得考慮過(guò)它們之間的種類差別。Python、C、C++、Java、Perl、Ruby、Smalltalk、Fortran、Basic 和 xBase 都是簡(jiǎn)單的命令式編程語(yǔ)言。其中,一些是面向?qū)ο蟮?,但那只是組織代碼和數(shù)據(jù)的問(wèn)題,而非基本編程樣式的問(wèn)題。使用這些語(yǔ)言,您 命令程序執(zhí)行指令序列:把某些數(shù)據(jù) 放入(put)變量中;從變量中 獲取(fetch)數(shù)據(jù); 循環(huán)(loop)一個(gè)指令塊 直到(until)滿足了某些條件; 如果(if)某個(gè)命題為 true,那么就進(jìn)行某些操作。所有這些語(yǔ)言的一個(gè)妙處在于:便于用日常生活中熟悉的比喻來(lái)考慮它們。日常生活都是由做事、選擇、再做另一件事所組成的,期間或許會(huì)使用一些工具??梢院?jiǎn)單地將運(yùn)行程序的計(jì)算機(jī)想象成廚師、瓦匠或汽車司機(jī)。
諸如 Prolog、Mercury、SQL、XSLT 這樣的語(yǔ)言、EBNF 語(yǔ)法和各種格式的真正配置文件,都 聲明某事是這種情況,或者應(yīng)用了某些約束。函數(shù)語(yǔ)言(比如 Haskell、ML、Dylan、Ocaml 和 Scheme)與此相似,但是它們更加強(qiáng)調(diào)陳述編程對(duì)象(遞歸、列表,等等)之間的內(nèi)部(函數(shù))關(guān)系。我們的日常生活(至少在敘事質(zhì)量方面)沒(méi)有提供對(duì)這些語(yǔ)言的編程構(gòu)造的直接模擬。然而,對(duì)于那些可以用這些語(yǔ)言進(jìn)行描述的問(wèn)題來(lái)說(shuō),聲明性描述 遠(yuǎn)遠(yuǎn)比命令式解決方案來(lái)得簡(jiǎn)明且不易出錯(cuò)。例如,請(qǐng)研究下面這個(gè)線性方程組:
清單 1. 線性方程式系統(tǒng)樣本
10x + 5y - 7z + 1 = 0 17x + 5y - 10z + 3 = 0 5x - 4y + 3z - 6 = 0
這是個(gè)相當(dāng)漂亮的說(shuō)明對(duì)象(x、y 和 z)之間幾個(gè)關(guān)系的簡(jiǎn)單表達(dá)式。在現(xiàn)實(shí)生活中您可能會(huì)用不同的方式求出這些答案,但是實(shí)際上用筆和紙“求解 x”很煩,而且容易出錯(cuò)。從調(diào)試角度來(lái)講,用 Python 編寫求解步驟或許會(huì)更糟糕。
Prolog 是與邏輯或數(shù)學(xué)關(guān)系密切的語(yǔ)言。使用這種語(yǔ)言,您只要編寫您知道是正確的語(yǔ)句,然后讓應(yīng)用程序?yàn)槟贸鼋Y(jié)果。語(yǔ)句不是按照特定的順序構(gòu)成的(和線性方程式一樣,沒(méi)有順序),而且您(程序員或用戶)并不知道得出的結(jié)果都采用了哪些步驟。例如:
清單 2. family.pro Prolog 樣本
/* Adapted from sample at: <http://www.engin.umd.umich.edu/CIS/course.des/cis479/prolog/> This app can answer questions about sisterhood & love, e.g.: # Is alice a sister of harry? ?-sisterof( alice, harry ) # Which of alice' sisters love wine? ?-sisterof( X, alice ), love( X, wine) */ sisterof( X, Y ) :- parents( X, M, F ), female( X ), parents( Y, M, F ). parents( edward, victoria, albert ). parents( harry, victoria, albert ). parents( alice, victoria, albert ). female( alice ). loves( harry, wine ). loves( alice, wine ).
它和 EBNF(擴(kuò)展巴科斯范式,Extended Backus-Naur Form)語(yǔ)法聲明并不完全一樣,但是實(shí)質(zhì)相似。您可以編寫一些下面這樣的聲明:
清單 3. EBNF 樣本
word := alphanums, (wordpunct, alphanums)*, contraction? alphanums := [a-zA-Z0-9]+ wordpunct := [-_] contraction := "'", ("clock"/"d"/"ll"/"m"/"re"/"s"/"t"/"ve")
如果您遇到一個(gè)單詞而想要表述其看上去 可能會(huì)是什么,而實(shí)際上又不想給出如何識(shí)別它的序列指令,上面便是個(gè)簡(jiǎn)練的方法。正則表達(dá)式與此相似(并且事實(shí)上它能夠滿足這種特定語(yǔ)法產(chǎn)品的需要)。
還有另一個(gè)聲明性示例,請(qǐng)研究描述有效 XML 文檔方言的文檔類型聲明:
清單 4. XML 文檔類型聲明
<!ELEMENT dissertation (chapter+)> <!ELEMENT chapter (title, paragraph+)> <!ELEMENT title (#PCDATA)> <!ELEMENT paragraph (#PCDATA | figure)+> <!ELEMENT figure EMPTY>
和其它示例一樣,DTD 語(yǔ)言不包含任何有關(guān)如何識(shí)別或創(chuàng)建有效 XML 文檔的指令。它只描述了如果文檔存在,那它會(huì)是怎么樣的。聲明性語(yǔ)言采用虛擬語(yǔ)氣。
Python 作為解釋器 vs Python 作為環(huán)境
Python 庫(kù)可以通過(guò)兩種截然不同的方式中的一種來(lái)利用聲明性語(yǔ)言?;蛟S更為常用的技術(shù)是將非 Python 聲明性語(yǔ)言作為數(shù)據(jù)來(lái)解析和處理。應(yīng)用程序或庫(kù)可以讀入外部來(lái)源(或者是內(nèi)部定義的但只用作“blob”的字符串),然后指出一組要執(zhí)行的命令式步驟,這些步驟在某種形式上與那些外部聲明是一致的。本質(zhì)上,這些類型的庫(kù)是“數(shù)據(jù)驅(qū)動(dòng)的”系統(tǒng);聲明性語(yǔ)言和 Python 應(yīng)用程序執(zhí)行或利用其聲明的操作之間有著概念和范疇差別。事實(shí)上,相當(dāng)普遍的一點(diǎn)是,處理那些相同聲明的庫(kù)也被用來(lái)實(shí)現(xiàn)其它編程語(yǔ)言。
上面給出的所有示例都屬于第一種技術(shù)。庫(kù) PyLog 是 Prolog 系統(tǒng)的 Python 實(shí)現(xiàn)。它讀取像樣本那樣的 Prolog 數(shù)據(jù)文件,然后創(chuàng)建 Python 對(duì)象來(lái)對(duì) Prolog 聲明 建模。EBNF 樣本使用專門變體 SimpleParse ,這是一個(gè) Python 庫(kù),它將這些聲明轉(zhuǎn)換成可以被 mx.TextTools 所使用的狀態(tài)表。 mx.TextTools 自身是 Python 的擴(kuò)展庫(kù),它使用底層 C 引擎來(lái)運(yùn)行存儲(chǔ)在 Python 數(shù)據(jù)結(jié)構(gòu)中的代碼,但與 Python 本質(zhì)上幾乎沒(méi)什么關(guān)系。對(duì)于這些任務(wù)而言,Python 是極佳的 粘合劑,但是粘合在一起的語(yǔ)言與 Python 差別很大。而且,大多數(shù) Prolog 實(shí)現(xiàn)都不是用 Python 編寫的,這和大多數(shù) EBNF 解析器一樣。
DTD 類似于其它示例。如果您使用象 xmlproc 這樣的驗(yàn)證解析器,您可以利用 DTD 來(lái)驗(yàn)證 XML 文檔的方言。但是 DTD 的語(yǔ)言并不是 Python 式的, xmlproc 只將它用作需要解析的數(shù)據(jù)。而且,已經(jīng)用許多編程語(yǔ)言編寫過(guò) XML 驗(yàn)證解析器。XSLT 轉(zhuǎn)換與此相似,也不是特定于 Python 的,而且像 ft.4xslt 這樣的模塊只將 Python 用作“粘合劑”。
雖然上面的方法和上面所提到的工具(我一直都在使用)都沒(méi)什么 不對(duì),但如果 Python 本身是聲明性語(yǔ)言的話,那么它可能會(huì)更精妙,而且某些方面會(huì)表達(dá)得更清晰。如果沒(méi)有其它因素的話,有助于此的庫(kù)不會(huì)使程序員在編寫一個(gè)應(yīng)用程序時(shí)考慮是否采用兩種(或更多)語(yǔ)言。有時(shí),依靠 Python 的自省能力來(lái)實(shí)現(xiàn)“本機(jī)”聲明,既簡(jiǎn)單又管用。
自省的魔力
解析器 Spark 和 PLY 讓用戶 用 Python 來(lái)聲明 Python 值,然后使用某些魔法來(lái)讓 Python 運(yùn)行時(shí)環(huán)境進(jìn)行解析配置。例如,讓我們研究一下與前面 SimpleParse 語(yǔ)法等價(jià)的 PLY 語(yǔ)法。 Spark 類似于下面這個(gè)示例:
清單 5. PLY 樣本
tokens = ('ALPHANUMS','WORDPUNCT','CONTRACTION','WHITSPACE') t_ALPHANUMS = r"[a-zA-Z0-0]+" t_WORDPUNCT = r"[-_]" t_CONTRACTION = r"'(clock|d|ll|m|re|s|t|ve)" def t_WHITESPACE(t): r"\s+" t.value = " " return t import lex lex.lex() lex.input(sometext) while 1: t = lex.token() if not t: break
我已經(jīng)在我即將出版的書籍 Text Processing in Python 中編寫了有關(guān) PLY 的內(nèi)容,并且在本專欄文章中編寫了有關(guān) Spark 的內(nèi)容(請(qǐng)參閱 參考資料以獲取相應(yīng)鏈接)。不必深入了解庫(kù)的詳細(xì)信息,這里您應(yīng)當(dāng)注意的是:正是 Python 綁定本身配置了解析(在這個(gè)示例中實(shí)際是詞法分析/標(biāo)記化)。 PLY 模塊在 Python 環(huán)境中運(yùn)行以作用于這些模式聲明,因此就正好非常了解該環(huán)境。
PLY如何得知它自己做什么,這涉及到一些非常奇異的 Python 編程。起初,中級(jí)程序員會(huì)發(fā)現(xiàn)可以查明 globals() 和 locals() 字典的內(nèi)容。如果聲明樣式略有差異的話就好了。例如,假想代碼更類似于這樣:
清單 6. 使用導(dǎo)入的模塊名稱空間
import basic_lex as _ _.tokens = ('ALPHANUMS','WORDPUNCT','CONTRACTION') _.ALPHANUMS = r"[a-zA-Z0-0]+" _.WORDPUNCT = r"[-_]" _.CONTRACTION = r"'(clock|d|ll|m|re|s|t|ve)" _.lex()
這種樣式的聲明性并不差,而且可以假設(shè) basic_lex 模塊包含類似下面這樣的簡(jiǎn)單內(nèi)容:
清單 7. basic_lex.py
def lex(): for t in tokens: print t, '=', globals()[t]
這會(huì)產(chǎn)生:
% python basic_app.py ALPHANUMS = [a-zA-Z0-0]+ WORDPUNCT = [-_] CONTRACTION = '(clock|d|ll|m|re|s|t|ve)
PLY 設(shè)法使用堆棧幀信息插入了導(dǎo)入模塊的名稱空間。例如:
清單 8. magic_lex.py
import sys try: raise RuntimeError except RuntimeError: e,b,t = sys.exc_info() caller_dict = t.tb_frame.f_back.f_globals def lex(): for t in caller_dict['tokens']: print t, '=', caller_dict['t_'+t]
這產(chǎn)生了與 basic_app.py 樣本所給輸出一樣的輸出,但是具有使用前面 t_TOKEN 樣式的聲明。
實(shí)際的 PLY 模塊中要比這更神奇。我們看到用模式 t_TOKEN 命名的標(biāo)記實(shí)際上可以是包含了正則表達(dá)式的字符串,或是包含了正則表達(dá)式文檔字符串和操作代碼的函數(shù)。某些類型檢查允許以下多態(tài)行為:
清單 9. polymorphic_lex
# ...determine caller_dict using RuntimeError... from types import * def lex(): for t in caller_dict['tokens']: t_obj = caller_dict['t_'+t] if type(t_obj) is FunctionType: print t, '=', t_obj.__doc__ else: print t, '=', t_obj
顯然,相對(duì)于用來(lái)玩玩的示例而言,真正的 PLY 模塊用這些已聲明的模式可以做更有趣的事,但是這些示例演示了其中所涉及的一些技術(shù)。
繼承的魔力
讓支持庫(kù)到處插入并操作應(yīng)用程序的名稱空間,這會(huì)啟用精妙的聲明性樣式。但通常,將繼承結(jié)構(gòu)和自省一起使用會(huì)使靈活性更佳。
模塊 gnosis.xml.validity 是用來(lái)創(chuàng)建直接映射到 DTD 產(chǎn)品的類的框架。任何 gnosis.xml.validity 類 只能用符合 XML 方言有效性約束的參數(shù)進(jìn)行實(shí)例化。實(shí)際上,這并不十分正確;當(dāng)只存在一種明確的方式可將參數(shù)“提升”成正確類型時(shí),模塊也可從更簡(jiǎn)單的參數(shù)中推斷出正確類型。
由于我已經(jīng)編寫了 gnosis.xml.validity 模塊,所以我傾向于思考其用途自身是否有趣。但是對(duì)于本文,我只想研究創(chuàng)建有效性類的聲明性樣式。與前面的 DTD 樣本相匹配的一組規(guī)則/類包括:
清單 10. gnosis.xml.validity 規(guī)則聲明
from gnosis.xml.validity import * class figure(EMPTY): pass class _mixedpara(Or): _disjoins = (PCDATA, figure) class paragraph(Some): _type = _mixedpara class title(PCDATA): pass class _paras(Some): _type = paragraph class chapter(Seq): _order = (title, _paras) class dissertation(Some): _type = chapter
您可以使用以下命令從這些聲明中創(chuàng)建出實(shí)例:
ch1 = LiftSeq(chapter, ("1st Title","Validity is important")) ch2 = LiftSeq(chapter, ("2nd Title","Declaration is fun")) diss = dissertation([ch1, ch2]) print diss
請(qǐng)注意這些類和前面的 DTD 非常匹配。映射基本上是一一對(duì)應(yīng)的;除了有必要對(duì)嵌套標(biāo)記的量化和交替使用中介體之外(中介體名稱用前導(dǎo)下劃線標(biāo)出來(lái))。
還要注意的是,這些類雖然是用標(biāo)準(zhǔn) Python 語(yǔ)法創(chuàng)建的,但它們也有不同尋常(且更簡(jiǎn)練)之處:它們沒(méi)有方法或?qū)嵗龜?shù)據(jù)。單獨(dú)定義類,以便從某框架繼承類,而該框架受到單一的類屬性限制。例如, <chapter> 是其它標(biāo)記序列,即 <title> 后面跟著一個(gè)或多個(gè) <paragraph> 標(biāo)記。但是為確保在實(shí)例中遵守約束,我們所需做的就是用這種簡(jiǎn)單的方式來(lái) 聲明chapter 類。
編寫像 gnosis.xml.validity.Seq 這樣的父類程序所涉及的主要“技巧”,就是在初始化期間研究 實(shí)例的 .__class__ 屬性。類 chapter 自身并不進(jìn)行初始化,因此調(diào)用其父類的 __init__() 方法。但是傳遞給父類 __init__() 的 self 是 chapter 的實(shí)例,而且 self 知道 chapter。為了舉例說(shuō)明這一點(diǎn),下面列出了部分 gnosis.xml.validity.Seq 實(shí)現(xiàn):
清單 11. 類 gnosis.xml.validity.Seq
class Seq(tuple): def __init__(self, inittup): if not hasattr(self.__class__, '_order'): raise NotImplementedError, \ "Child of Abstract Class Seq must specify order" if not isinstance(self._order, tuple): raise ValidityError, "Seq must have tuple as order" self.validate() self._tag = self.__class__.__name__
一旦應(yīng)用程序程序員試圖創(chuàng)建 chapter 實(shí)例,實(shí)例化代碼就檢查是否用所要求的 ._order 類屬性聲明了 chapter ,并檢查該屬性是否為所需的元組對(duì)象。方法 .validate() 要做進(jìn)一步的檢查,以確保初始化實(shí)例所用的對(duì)象屬于 ._order 中指定的相應(yīng)類。
何時(shí)聲明
聲明性編程樣式在聲明約束方面 幾乎一直比命令式或過(guò)程式樣式更直接。當(dāng)然,并非所有的編程問(wèn)題都是關(guān)于約束的 - 或者說(shuō)至少這并非總是自然定律。但是如果基于規(guī)則的系統(tǒng)(比如語(yǔ)法和推理系統(tǒng))可以進(jìn)行聲明性描述,那么它們的問(wèn)題就比較容易處理了。是否符合語(yǔ)法的命令式驗(yàn)證很快就會(huì)變成非常復(fù)雜難懂的所謂“意大利面條式代碼”(spaghetti code),而且很難調(diào)試。模式和規(guī)則的聲明仍然可以更簡(jiǎn)單。
當(dāng)然,起碼在 Python 中,聲明規(guī)則的驗(yàn)證和增強(qiáng)總是會(huì)歸結(jié)為過(guò)程式檢查。但是把這種過(guò)程式檢查放在進(jìn)行了良好測(cè)試的庫(kù)代碼中比較合適。單獨(dú)的應(yīng)用程序應(yīng)該依靠由像 Spark 或 PLY 或 gnosis.xml.validity 這樣的庫(kù)所提供的更簡(jiǎn)單的聲明性接口。其它像 xmlproc 、 SimpleParse 或 ft.4xslt 這樣的庫(kù),盡管不是 用 Python進(jìn)行聲明的(Python 當(dāng)然適用于它們的領(lǐng)域),也能使用聲明性樣式。
相關(guān)文章
淺析Python 簡(jiǎn)單工廠模式和工廠方法模式的優(yōu)缺點(diǎn)
這篇文章主要介紹了Python 工廠模式的相關(guān)資料,文中示例代碼非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07PyTorch中torch.tensor與torch.Tensor的區(qū)別詳解
這篇文章主要介紹了PyTorch中torch.tensor與torch.Tensor的區(qū)別詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05Python中print函數(shù)語(yǔ)法格式以及各參數(shù)舉例詳解
這篇文章主要給大家介紹了關(guān)于Python中print函數(shù)語(yǔ)法格式以及各參數(shù)舉例詳解的相關(guān)資料,print()函數(shù)用于將指定的字符串或?qū)ο?通常是字符串)輸出到屏幕或文件中,需要的朋友可以參考下2023-10-10利用 Python ElementTree 生成 xml的實(shí)例
這篇文章主要介紹了利用 Python ElementTree 生成 xml的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03為何人工智能(AI)首選Python?讀完這篇文章你就知道了(推薦)
這篇文章主要介紹了為何人工智能(AI)首選Python,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04在Python的while循環(huán)中使用else以及循環(huán)嵌套的用法
這篇文章主要介紹了在Python的while循環(huán)中使用else以及循環(huán)嵌套的用法,是Python入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-10-10python讀取excel表格生成erlang數(shù)據(jù)
這篇文章主要為大家詳細(xì)介紹了python讀取excel表格生成erlang數(shù)據(jù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08Python count()函數(shù)實(shí)例詳解
count() 是Python的內(nèi)置函數(shù),可以「統(tǒng)計(jì)」字符串里指定「字符」或指定字符串出現(xiàn)的「次數(shù)」,這篇文章主要介紹了Python count()函數(shù)詳解,需要的朋友可以參考下2023-07-07