摘要:最近在閱讀微型框架的源碼,發現了中有一個既是裝飾器類又是描述符的有趣實現。所以第三版的代碼可以這樣寫第三版的代碼沒有使用裝飾器,而是使用了描述符這個技巧。更大的問題來自如何將描述符與裝飾器結合起來,因為是一個類而不是方法。
最近在閱讀Python微型Web框架Bottle的源碼,發現了Bottle中有一個既是裝飾器類又是描述符的有趣實現。剛好這兩個點是Python比較的難理解,又混合在一起,讓代碼有些晦澀難懂。但理解代碼之后不由得為Python語言的簡潔優美贊嘆。所以把相關知識和想法稍微整理,以供分享。
正文Bottle是Python的一個微型Web框架,所有代碼都在一個bottle.py文件中,只依賴標準庫實現,兼容Python 2和Python 3,而且最新的穩定版0.12代碼也只有3700行左右。雖然小,但它實現了Web框架基本功能。這里就不以過多的筆墨去展示Bottle框架,需要的請訪問其網站了解更多。這里著重介紹與本文相關的重要對象request。在Bottle里,request對象代表了當前線程處理的請求,客戶端發送的請求數據如表單數據,請求網站和cookie都可以從request對象中獲得。下面是官方文檔中的兩個例子
from bottle import request, route, response, template
# 獲取客戶端cookie以實現登陸時問候用戶功能 @route("/hello") def hello(): name = request.cookie.username or "Guest" return template("Hello {{name}}", name=name) # 獲取形如/forum?id=1&page=5的查詢字符串中id和page變量的值 route("/forum") def display_forum(): forum_id = request.query.id page = request.query.page or "1" return template("Forum ID: {{id}} (page {{page}})", id=forum_id, page=page)
那么Bottle是如何實現的呢?根據WSGI接口規定,所有的HTTP請求信息都包含在一個名為envrion的dict對象中。所以Bottle要做的就是把HTTP請求信息從environ解析出來。在深入Request類如何實現之前先要了解下Bottle的FormsDict。FormsDict與字典類相似,但擴展了一些功能,比如支持屬性訪問、一對多的鍵值對、WTForms支持等。它在Bottle中被廣泛應用,如上面的示例中cookie和query數據都以FormsDict存儲,所以我們可以用request.query.page的方式獲取相應屬性值。
下面是0.12版Bottle中Request類的部分代碼,0.12版中Request類繼承了BaseRequest,為了方便閱讀我把代碼合并在一起,同時還有重要的DictProperty的代碼。需要說明的是Request類__init__傳入的environ參數就是WSGI協議中包含HTTP請求信息的envrion,而query方法中的_parse_qsl函數可以接受形如/forum?id=1&page=5原始查詢字符串然后以[(key1, value1), (ke2, value2), ...]的list返回。
class DictProperty(object): """ Property that maps to a key in a local dict-like attribute. """ def __init__(self, attr, key=None, read_only=False): self.attr, self.key, self.read_only = attr, key, read_only def __call__(self, func): functools.update_wrapper(self, func, updated=[]) self.getter, self.key = func, self.key or func.__name__ return self def __get__(self, obj, cls): if obj is None: return self key, storage = self.key, getattr(obj, self.attr) if key not in storage: storage[key] = self.getter(obj) return storage[key] def __set__(self, obj, value): if self.read_only: raise AttributeError("Read-Only property.") getattr(obj, self.attr)[self.key] = value def __delete__(self, obj): if self.read_only: raise AttributeError("Read-Only property.") del getattr(obj, self.attr)[self.key] class Request: def __init__(self, environ=None): self.environ {} if environ is None else envrion self.envrion["bottle.request"] = self @DictProperty("environ", "bottle.request.query", read_only=True) def query(self): get = self.environ["bottle.get"] = FormsDict() pairs = _parse_qsl(self.environ.get("QUERY_STRING", "")) for key, value in pairs: get[key] = value return get
query方法的邏輯和代碼都比較簡單,就是從environ中獲取"QUERY_STRING",并用把原始查詢字符串解析為一個FormsDict,將這個FormsDict賦值給environ["bottle.request.query"]并返回。但這個函數的裝飾器的作用就有些難以理解,裝飾器的實現方式都是"dunder"特殊方法,有些晦澀難懂。如果上來就看這些源碼可能難以理解代碼實現的功能。那不如這些放一邊,假設自己要實現這些方法,你會寫出什么代碼。
一開始你可能寫出這樣的代碼。
# version 1 class Request: """ some codes here """ def query(self): get = self.environ["bottle.get"] = FormsDict() pairs = _parse_qsl(self.environ.get("QUERY_STRING", "")) for key, value in pairs: get[key] = value return get
這樣確實實現了解析查詢字符串的功能,但每次在調用這個方法時都需要對原始查詢字符串解析一次,實際上在處理某特請求時,查詢字符串是不會改變的,所以我們只需要解析一次并把它保存起來,下次使用時直接返回就好了。另外此時的query方法還是一個普通方法,必須使用這樣的方法來調用它
# 獲取id request.query().id # 獲取page request.query().page
query后面的小括號讓語句顯得不那么協調,其實就是我覺得它丑。要是也能和官方文檔中的示例實現以屬性訪問的方式獲取相應的數據就好了。所以代碼還得改改。
# query method version 2 class Request: """ some codes here """ @property def query(self): if "bootle.get.query" not in self.environ: get = self.environ["bottle.get"] = FormsDict() pairs = _parse_qsl(self.environ.get("QUERY_STRING", "")) for key, value in pairs: get[key] = value return self.environ["bottle.get.query"]
第二版改變的代碼就兩處,一個是使用property裝飾器,實現了request.query的訪問方式;另一個就是在query函數體中增加了判斷"bottle.get.query"是否在environ中的判斷語句,實現了只解析一次的要求。第二版幾乎滿足了所有要求,它表現得就像Bottle中真正的query方法一樣。但它還是有些缺陷。
首先,Request類并不只有query一個方法,如果要編寫完整的Request類就會發現,有很多方法的代碼與query相似,都是從environ中解析出需要的數據,而且都只需要解析一次,保存起來,第二次或以后訪問時返回保存的數據就好了。所以可以考慮將屬性管理的代碼從方法體內抽象出來,正好Python中的描述符可以實現這樣的功能。另外如果使用Bottle的開發者在寫代碼時不小心嘗試進行request.query = some_data的賦值時,將會拋出如下錯誤。
>>> AttributeError: can"t set attribute
我們確實希望屬性是只讀的,在對其賦值時應該拋出錯誤,但這樣的報錯信息并沒有提供太多有用的信息,導致調bug時一頭霧水,找不到方向。我們更希望拋出如
>>> AttributeError: Read-only property
這樣明確的錯誤信息。
所以第三版的代碼可以這樣寫
# query method version 3 class Descriptor: def __init__(self, attr, key, getter, read_only=False): self.attr = attr self.key = key self.getter = getter self.read_only = read_only def __set__(self, obj, value): if self.read_only: raise AttributeError("Read only property.") getattr(obj, self.attr)[self.key] = value def __get__(self, obj, cls): if obj is None: return self key, storage = self.key, getattr(obj, self.attr) if key not in storage: storage[key] = self.getter(obj) return storage[key] def __delete__(self, obj): if self.read_only: raise AttributeError("Read only property.") del getattr(obj, self.attr)[self.key] class Reqeust: """ some codes """ def query(self): get = self.environ["bottle.get"] = FormsDict() pairs = _parse_qsl(self.environ.get("QUERY_STRING", "")) for key, value in pairs: get[key] = value return get query = Descriptor("environ", "bottle.get.query", query, read_only=True)
第三版的代碼沒有使用property裝飾器,而是使用了描述符這個技巧。如果你之前沒有見到過描述符,在這里限于篇幅只能做個簡單的介紹,但描述符涉及知識點眾多,如果有不清楚之處可以看看《流暢的Python》第20章屬性描述符,里面有非常詳細的介紹。
簡單來說,描述符是對多個屬性運用相同存取邏輯的一種方式,如Bottle框架里我們需要對很多屬性都進行判斷某個鍵是否在environ中,如果在則返回,如果不在,需要解析一次這樣的存取邏輯。而描述符需要實現特定協議,包括__set__, __get__, __delete___方法,分別對應設置,讀取和刪除屬性的方法。他么的參數也比較特殊,如__get__方法的三個參數self, obj, cls分別對應描述符實例的引用,對第三版的代碼來說就是Descriptor("environ", "bottle.get.query", query, read_only=True)創建的實例的引用;obj則對應將某個屬性托管給描述的實例對象的引用,對應的應該為request對象;而cls則為Request類的引用。在調用request.query時編譯器會自動傳入這些參數。如果以Request.query的方式調用,那么obj參數的傳入值為None,這時候通常的處理是返回描述符實例。
在Descriptor中__get__方法的代碼最多,也比較難理解,但如果記住其參數的意義也沒那么難。下面以query的實現為例,我添加一些注釋來幫助理解
key, storage = self.key, getattr(obj, self.attr) # key="bottle.get.query" # storage = environ 即包含HTTP請求的信息的environ # 判斷envrion中是否包含key來決定是否需要解析 if key not in storage: storage[key] = self.getter(obj) # self.getter(obj)就是調用了原來的query方法,不過要傳入一個Request實例,也就是obj return storage[key]
而__set__, __delete__代碼比較簡單,在這里我們把只讀屬性在賦值和刪除時拋出的錯誤定制為AttributeError("Read only property."),方便調試。
通過使用描述符這個有些難懂的方法,我們可以在Request的方法中專心于編寫如何解析的代碼,不用擔心屬性的存取邏輯。和在每個方法中都使用if判斷相比高到不知道哪里去。但美中不足的是,這樣讓我們的方法代碼后面拖著一個“小尾巴”,即
query = Descriptor("envrion", "bottle.get.query", query, read_only=True)
怎么去掉這個這個“小尾巴“呢?回顧之前的代碼幾乎都是對query之類的方法進行修飾,所以可以嘗試使用裝飾器,畢竟裝飾器就是對某個函數進行修飾的,而且我們應該使用參數化的裝飾器,這樣才能將envrion等參數傳遞給裝飾器。如果要實現參數化裝飾器就需要一個裝飾器工廠函數,也就是說裝飾器的代碼里需要嵌套至少3個函數體,寫起來有寫繞,代碼可閱讀性也有差。更大的問題來自如何將描述符與裝飾器結合起來,因為Descriptor是一個類而不是方法。
解決辦法其實挺簡單的。如果知道Python中函數也是對象,實現了__call__方法的對象可以表現得像函數一樣。所以我們可以修改Descirptor的代碼,實現__call__方法,讓它的實例成為callable對象就可以把它用作裝飾器;而要傳入的參數可以以實例屬性存儲起來,通過self.attribute的形式訪問,而不是像使用工廠函數實現參數化裝飾器時通過閉包來實現參數的訪問獲取。這時候再來看看Bottle里的DictProperty代碼
class DictProperty(object): """ Property that maps to a key in a local dict-like attribute. """ def __init__(self, attr, key=None, read_only=False): self.attr, self.key, self.read_only = attr, key, read_only def __call__(self, func): functools.update_wrapper(self, func, updated=[]) self.getter, self.key = func, self.key or func.__name__ return self def __get__(self, obj, cls): if obj is None: return self key, storage = self.key, getattr(obj, self.attr) if key not in storage: storage[key] = self.getter(obj) return storage[key] def __set__(self, obj, value): if self.read_only: raise AttributeError("Read-Only property.") getattr(obj, self.attr)[self.key] = value def __delete__(self, obj): if self.read_only: raise AttributeError("Read-Only property.") del getattr(obj, self.attr)[self.key]
其實就是一個有描述符作用的裝飾器類,它的使用方法很簡單:
@DictProperty("environ", "bottle.get.query", read_only=True) def query(self): """ some codes """
拆開會更好理解點:
property = DictProperty("environ", "bottle.get.query", read_only=True) @property def query(self): """ some codes """
再把@實現的語法糖拆開:
def query(self): """ some codes """
property = DictProperty("environ", "bottle.get.query", read_only=True)
query = property(query) # @實現的語法糖
再修改以下代碼形式:
def query(self): """ some codes """
query = DictProperty("environ", "bottle.get.query", read_only=True)(query)
是不是和第三版的實現方式非常相似。
def query(self): """ some codes """ query = Descriptor("environ", "bottle.get.query", query, read_only=True)
但我們可以使用裝飾器把方法體后面那個不和諧的賦值語句”小尾巴“去掉,將屬性存取管理抽象出來,而且只需要使用一行非常簡便的裝飾器把這個功能添加到某個方法上。這也許就是Python的美之一吧。
DictProperty涉及知識遠不止文中涉及的那么簡單,如果你還是不清楚DictProperty的實現功能,建議閱讀《流暢的Python》第7章和第22章,對裝飾器和描述符有詳細的描述,另外《Python Cookbook》第三版第9章元編程有關于參數化裝飾器和裝飾器類的敘述和示例。如果你對Bottle為什么要實現這樣的功能感到困惑,建議閱讀Bottle的文檔和WSGI相關的文章。
其實前一陣再閱讀Bottle源碼時就想寫一篇文章,但奈何許久不寫東西文筆生疏加上醫院實習期間又比較忙,一直推到現在才終于磕磕絆絆地把我閱讀的Bottle源碼的一些感悟寫出來,希望對喜歡Python的各位有些幫助把。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/40866.html
摘要:安裝是一個輕量型的不依賴于任何第三方庫的框架,整個框架只有一個文件。向打聲招呼吧新建一個文件在瀏覽器或者,,得到結果當使用裝飾器綁定路由時,實際是使用了的默認應用,即是的一個實例。 1. 安裝 bottle是一個輕量型的不依賴于任何第三方庫的web框架,整個框架只有bottle.py一個文件。 wget http://bottlepy.org/bottle.py 2. 向bottl...
摘要:在源碼閱讀一中,我們了解了如何接收請求,處理請求以及如何檢測模塊變化重啟。接下來我們看一下源碼是怎么實現的經過封裝后,最終獲得的是具備有一些屬性的裝飾器當為時,將的屬性傳遞給,使其具備相同的屬性。 在《Bottle源碼閱讀(一)》中,我們了解了bottle如何接收請求,處理請求以及如何檢測模塊變化重啟server。在ServerHandler類中的run函數中,application接...
摘要:而其他的引擎,例如能夠幫我們進行驗證登錄自此,官網的我們已經大致有了了解后續我們可以選擇運用該框架實現一些簡單的應用,或者可以深入研究其源碼,提升自身的編程水平 在初識Bottle(一)中,我們了解了Bottle的基本用法在Bottle源碼閱讀(一)和Bottle源碼閱讀(二)可以查看個人對bottle源碼的相關閱讀筆記 下面繼續閱讀Bottle的官方文檔https://bottlep...
摘要:的裝飾器中的同樣借鑒了這個語法糖,不過依賴于的方法。等同于也就是說,裝飾器是一個對類進行處理的函數。別名或裝飾器在控制臺顯示一條警告,表示該方法將廢除。有了裝飾器,就可以改寫上面的代碼。 更多文章,請在Github blog查看 在 ES6 中增加了對類對象的相關定義和操作(比如 class 和 extends ),這就使得我們在多個不同類之間共享或者擴展一些方法或者行為的時候,變得并...
摘要:裝飾器模式允許向一個現有的對象添加新的功能,同時又不改變其結構。這便是裝飾模式,通過一層一層的裝飾,我們可以靈活的得到我們想要的結果。可以輕松的添加新的裝飾器類或者新的組件來創建靈活的結構。 前言 在編碼的時候,我們為了擴展一個類經常是使用繼承方式來實現,隨著擴展功能的增多,子類會越來越膨脹,使系統變得不靈活。 裝飾器模式( Decorator Pattern )允許向一個現有的對象添...
閱讀 1847·2021-09-22 15:23
閱讀 3282·2021-09-04 16:45
閱讀 1911·2021-07-29 14:49
閱讀 2780·2019-08-30 15:44
閱讀 1529·2019-08-29 16:36
閱讀 1051·2019-08-29 11:03
閱讀 1521·2019-08-26 13:53
閱讀 516·2019-08-26 11:57