Lunar, 一個Python網絡框架的實現

前先後後,大概兩個月的時間,lunar這個項目終於達到了一個很高的完整度。html

Lunar是一個Python語言的網絡框架,相似於Django,Flask,Tornado等當下流行的web framework。最初有這個想法是在大二下學期,當時接觸Python web編程有一段時間。最先接觸Python web編程或許是在大一下?自覺當時編程尚未入門,第一個接觸的web框架是Django,很龐大的框架,當時很low逼的去看那本Django Book的中文譯本,翻譯的其實很不錯了,只是進度會落後於當前的版本,因此在跑example的時候會有一些問題,Django的龐大規模給我留下了很大的心理陰影,因此在以後,對於涉世未深的Pythoner,我可能都不會推薦Django做爲第一個Python的網絡框架來學習。python

整個框架的挑戰仍是很是大的。核心的幾個組件(模板引擎,ORM框架,請求和應答的處理)仍是有一些難度,可是通過一步步的分析和編碼仍是可以完成功能。項目受Flask很是大的影響,最初做爲造輪子的初衷,幾乎完整的使用了Flask和SQLAlchemy的API接口。git

項目一樣開源在Github上: https://github.com/jasonlvhit/lunargithub

也能夠經過pip直接安裝:web

$ pip install lunar

這裏我大概的記述一下lunar整個項目的各個組件的設計和實現。sql

ORM framework

首先是ORM。數據庫

python圈子裏面仍是有不少很著名的orm框架,SQLAlchemypeeweepony orm各有特點,SQLAlchemy和peewee都已經是很成熟的框架,大量的被應用在商業環境中。回到Lunar,既然是造輪子,何不造個完全,因而便擼了一個orm框架出來。django

在ORM框架中,咱們使用類的定義來表示數據庫中的表結構,使用類方法避免繁瑣的SQL語句,一個ORM類定義相似於下面這段代碼:編程

pyclass Post(db.Model):

    __tablename__ = 'post'

    id = database.PrimaryKeyField()
    title = database.CharField(100)
    content = database.TextField()
    pub_date = database.DateField()

    author_id = database.ForeignKeyField('author')
    tags = database.ManyToManyField(rel='post_tag_re', to_table='tag')

    def __repr__(self):
        return '<Post %s>' % self.title

上面這段代碼取自Lunar框架的ORM測試和一個博客的example,這段代碼定義了一個Post類,表明了數據庫中的一張表,類中的一系列屬性分別對應着表中的列數據。設計模式

一個peewee或者SQLAlchemy相似語法的一個ORM框架語句相似下面這樣:

pyp = Post.get(id=1)

返回的結果是Post類的實例,這個實例,p.id返回的不是一個PrimaryKeyField,而是一個int類型值,其餘的與數據庫關聯的類屬性也是一樣。

orm框架本質上是sql語句或者數據庫模式(schema)和python對象之間的轉換器或者翻譯器,這有些相似於編譯器結構。

在這裏,Post是咱們建立的一個orm類,post擁有若干數據操做方法,經過調用相似這樣的更加人性化或者直觀的api,代替傳統的sql語句和對象映射。orm框架將語句翻譯爲sql語句,執行,並在最後將語句轉換爲post類的實例。

可能從這個角度看來,實現orm框架並非什麼tough的任務,讓咱們用上面提到的這個例子來看

pyp = Post.get(id=1)

這條語句翻譯成的sql語句爲

select * from post where id=1;

能夠看到的是,get方法會使用一個select語句,翻譯程序將post類的表名和條件分別組合到select語句中,嗅覺靈敏的pythoner會發現這是一個典型的Post類的classmethod,直接經過類來調用這個方法,咱們能夠快速的寫出這個函數的僞代碼:

pyclass Model(Meta):

    ...

    @classmethod
    def get(cls, *args, **kwargs):
        # get method only supposes to be used by querying by id.
        # UserModel.get(id=2)
        # return a single instance.
        sql = "select * from %s"
        if kwargs:
            sql += "where %s"
        rs = db.execute(sql %(cls.__tablename__, ' and '.join(['='.join([k, v]) 
            for k, v in kwargs.items()])))
        return make_instance(rs, descriptor) #descriptor describe the format of rs.

從本質上,全部的翻譯工做均可以這樣來完成。可是在重構後的代碼中可能會掩蓋掉不少細節。

其實這大概是實現一個orm框架的所有了,只是咱們還須要一點python中很酷炫的一個編程概念來解決一個問題。

考慮實現ORM框架的create_all方法,建立全部ORM框架規範下類的實際數據庫表,這是熟悉SQLAlchemy的Pythoner都會比較熟悉的一個方法。

create_all方法要求全部繼承了db.Model類的子類所有註冊在db的一個屬性中,好比tabledict,這樣create_all方法在調用時可使用db中的tabledict屬性,將全部註冊的類編譯爲SQL語句並執行。

直觀的來看,咱們須要控制類建立的行爲。例如Post類,在這個類被建立的時候,將Post類寫入tabledict

那麼怎麼控制一個類被建立的時候的行爲?答案是使用元編程,Python中有多種實現元編程的方式,descriptor或者metaclass等方式都是實現元編程的方式,在這裏,咱們使用元類(metaclass)。關於metaclass,網絡上最經典的文章莫過於StackOverflow上的這篇回答,強烈推薦給全部的人看。這裏我先直接給出僞碼:

pyclass MetaModel(type):
    def __new__(cls, name, bases, attrs):
        cls = super(MetaModel, cls).__new__(cls, name, bases, attrs)

        ...

        cls_dict = cls.__dict__
        if '__tablename__' in cls_dict.keys():
            setattr(cls, '__tablename__', cls_dict['__tablename__'])
        else:
            setattr(cls, '__tablename__', cls.__name__.lower())

        if hasattr(cls, 'db'):
            getattr(cls, 'db').__tabledict__[cls.__tablename__] = cls

        ...

        return cls

class Model(MetaModel('NewBase', (object, ), {})): #python3 compatibility
    def __init__(self, **kwargs):

        ...

        for k, v in kwargs.items():
            setattr(self, k, v))

        ...

這種方式定義的Model,在建立的時候,會由MetaModel控制建立過程,最後返回整個類,在建立過程當中,咱們將表名稱和類自己所有塞入了db的一個屬性中。這樣create_all方法即可以直接使用tabledict中的類屬性直接建立全部的表:

pyclass Database(threading.local):
    ...

    def create_all(self):
        for k, v in self.__tabledict__.items():
            if issubclass(v, self.Model):
                self.create_table(v)

OK,到這裏,幾乎ORM的全部核心技術所有介紹完畢。ORM並非一個很tough的工做,可是也並非很簡單。ORM框架的實現是一個解決一系列問題的過程,其實思考的過程是最爲激動人心的。

模板引擎

模板引擎是另一個比較大和tough的模塊。Python一樣有不少出色的模板引擎,當下最爲流行莫過於MakoJinja2,國外的Reddit和國內的豆瓣公司大量的使用了Mako做爲模板引擎進行網頁渲染。Jinja2由於具備強大的性能支撐和沙箱模式,在Python社區中也很流行。

Python模板引擎的核心功能是把標記語言編譯成爲可執行的代碼,執行一些邏輯或者操做,返回模板文件的渲染結果,每每是字符串。模板引擎的實現一樣相似於傳統的編譯器結構,模板引擎首先會使用一個詞法分析模塊分析出全部的token,並分類標記;在這以後,會使用一個相似於編譯器中的語法分析的模塊分析token序列,調用相應的操做,對於不一樣的token,咱們須要單獨編寫一個處理程序(相似於SDT),來處理token的輸出。

最簡單的例子:

pyt = lunar.Template('Hello {{ name }}').render(name='lunar')

這段代碼,咱們期待的輸出是"Hello lunar",name會被lunar替換掉。根據上面我提到的模板引擎的工做流程,首先,咱們使用詞法分析程序對這段模板語言作模板編譯,分割全部的字符串(實際實現的時候並不是如此),給每一個單詞賦給一個屬性,例如上面這段模板語言通過最基礎的詞法分析會獲得下面這個結果:

<'Hello' PlainText>
<' ' Operator> # Blank Space
<'name' Variable>

有了「詞法分析」獲得的序列,咱們開始遍歷這個序列中的全部token,對每個token進行處理。

pyfor token in tokens:
    if isinstance(token, PlainText):
        processPlainText(token)
    elif isinstance(token, Variable):
        processVariable(token)

    ...

一些模板引擎將模板標記語言編譯爲Python代碼,使用exec函數執行,最後將結果嵌套回來。例如上面這段代碼,咱們能夠依次對token進行相似下面這樣的處理:

pydef processPlainText(token):
    return "_stdout.append('" +token+ "')"

def processVariable(token):
    return "_stdout.append(" + token +")"

看到這裏你可能會以爲莫名其妙,對於一連串的token序列,通過處理後的字符串相似於下面這樣,看完後你的狀態確定仍是莫名其妙:

pyintermediate = ""
intermediate += "_stdout.append('Hello')"
intermediate += "_stdout.append(' ')"
intermediate += "_stdout.append(name)"

回到上面提到的那個函數exec,咱們使用exec函數執行上面的這段字符串,這在本質上實際上是一種很危險的行爲。exec函數接受一個命名空間,或者說上下文(context)參數,咱們對這段代碼作相似下面的處理:

pycontext = {}
context['_stdout'] = []
context['name'] = 'lunar'

exec(intermediate, context)

return ''.join(context['_stdout'])

context是一個字典,在真正的模板渲染時,咱們把全部須要的上下文參數所有update到context中,傳給exec函數進行執行,exec函數會在context中進行更改,最後咱們能夠取到context中通過修改後的全部的值。在這裏,上面兩個代碼片斷中的_stdout在context中做爲一個空列表存在,因此在執行完exec後,context中的stdout會帶回咱們須要的結果。

具體來看,將render中的context傳入exec,這裏exec會執行一個變換:

_stdout.append(name) -> exec(intermediate, {name:'lunar'}) -> _stdout.append('lunar')

通過這個神奇的變化以後(搞毛,就是替換了一下嘛),咱們就獲得了模板渲染後須要的結果,一個看似是標記語言執行後的結果。

讓咱們看一個稍微複雜一些的模板語句,好比if...else...,通過處理後的中間代碼會相似於下面這樣:

<html>
{% if a > 2 %}
    {{ a }}
{% else %}
    {{ a * 3 }}
{% endif %}
</html>

中間代碼:

pyintermediate = "_stdout.append('<html>')\n"
intermediate += "if a > 2 :\n"
intermediate += "   _stdout.append(a)\n"
intermediate += "else :\n"
intermediate += "   _stdout.append(a * 3)\n"
intermediate += "_stdout.append('</html>')"

注意中間代碼中的縮進!這是這一類型的模板引擎執行控制流的全部祕密,這段代碼就是原生的Python代碼,可執行的Python代碼。模板引擎構建了一個從標記語言到Python原生語言的轉換器,因此模板引擎每每可以作出一些看似很嚇人,其實很low的功能,好比直接在模板引擎中寫lambda函數:

pyfrom lunar.template import Template

rendered = Template(
            '{{ list(map(lambda x: x * 2, [1, 2, 3])) }}').render()

可是!深刻優化後的模板引擎每每沒有這麼簡單,也不會使用這麼粗暴的實現方式,衆多模板引擎選擇了本身寫解釋程序。把模板語言編譯成AST,而後解析AST,返回結果。這樣作有幾點好處:

  • 自定義模板規則
  • 利於性能調優,好比C語言優化

固然,模板引擎界也有桑心病狂者,使用所有的C來實現,好比一樣頗有名的Cheetah。

或許由於代碼很小的緣由,我在Lunar中實現的這個模板引擎在多個benchmark測試下展示了還不錯的性能,具體的benchmark你們能夠在項目的template測試中找到,本身運行一下,這裏給出一個基於個人機器的性能測試結果:

第一個結果是Jonas BorgstrOm爲SpitFire所寫的benchmarks

Linux Platform
-------------------------------------------------------
Genshi tag builder                            239.56 ms
Genshi template                               133.26 ms
Genshi template + tag builder                 261.40 ms
Mako Template                                  44.64 ms
Djange template                               335.10 ms
Cheetah template                               29.56 ms
StringIO                                       33.63 ms
cStringIO                                       7.68 ms
list concat                                     3.25 ms
Lunar template                                 23.46 ms
Jinja2 template                                 8.41 ms
Tornado Template                               24.01 ms
-------------------------------------------------------

Windows Platform
-------------------------------------------------------
Mako Template                                 209.74 ms
Cheetah template                              103.80 ms
StringIO                                       42.96 ms
cStringIO                                      11.62 ms
list concat                                     4.22 ms
Lunar template                                 27.56 ms
Jinja2 template                                27.16 ms
-------------------------------------------------------

第二個結果是Jinja2中mitsuhiko的benchmark測試:

Linux Platform:
    ----------------------------------
    jinja               0.0052 seconds
    mako                0.0052 seconds
    tornado             0.0200 seconds
    django              0.2643 seconds
    genshi              0.1306 seconds
    lunar               0.0301 seconds
    cheetah             0.0256 seconds
    ----------------------------------

    Windows Platform:
    ----------------------------------
    ----------------------------------

    jinja               0.0216 seconds
    mako                0.0206 seconds
    tornado             0.0286 seconds
    lunar               0.0420 seconds
    cheetah             0.1043 seconds
    -----------------------------------

這個結果最吸引個人有下面幾點:

  • Jinja2真(TM)快!
  • Django真慢!
  • Mako的實現確定有特殊的優化點,不一樣的benchmark差距過大!

如今Lunar的代碼還很髒,並且能夠重構的地方還不少,相信重構後性能還會上一個臺階(誰知道呢?)。

Router

Router負責整個web請求的轉發,將一個請求地址和處理函數匹配在一塊兒。主流的Router有兩種接口類型,一種是Django和Tornado類型的"字典式":

pyurl_rules = {
    '/': index,
    '/post/\d': post,
}

另一種是Flask和Bottle這種小型框架偏心的裝飾器(decorator)類型的router:

py@app.route('/')
def index():
    pass

@app.route('/post/<int:id>')
def post(id):
    pass

router的實現仍是很簡單的,router的本質就是一個字典,把路由規則和函數鏈接在一塊兒。這裏有一些麻煩的是處理帶參數的路由函數,例如上例中,post的id是能夠從路由調用地址中直接得到的,調用/post/12會調用函數post(12),在這裏,傳參是較爲麻煩的一點。另外的一個難點是redirect和url_for的實現:

pyreturn redirect(url_for(post, 1))

但其實也不難啦,感興趣的能夠看一下代碼的實現。

Router的另一個注意點是,使用裝飾器方式實現的路由須要在app跑起來以前,讓函數都註冊到router中,因此每每須要一些很奇怪的代碼,例如我在Lunar項目的example中寫了一個blog,blog的init文件是像下面這樣定義的:

pyfrom lunar import lunar
from lunar import database

app = lunar.Lunar('blog')
app.config['DATABASE_NAME'] = 'blog.db'

db = database.Sqlite(app.config['DATABASE_NAME'])

from . import views

注意最後一行,最後一行代碼須要import views中的全部函數,這樣views中的函數纔會註冊到router中。這個痛點在Flask中一樣存在。

WSGI

最後的最後,咱們實現了這麼多組件,咱們仍是須要來實現Python請求中最核心和基本的東西,一個WSGI接口:

pydef app(environ, start_response):
     start_response('200 OK', [('Content-Type', 'text/plain')])
     yield "Hello world!\n"

WSGI接口很簡單,實現一個app,接受兩個參數environ和start_response,想返回什麼就返回什麼好了。關於WSGI的詳細信息,能夠查看PEP333PEP3333。這裏我說幾點對WSGI這個東西本身的理解:

計算機服務,或者說因特網服務的核心是什麼?如今的我,會給出協議這個答案。咱們會發現,計算機的底層,網絡通訊的底層都是很簡單、很樸素的東西,無非是一些0和1,一些所謂字符串罷了。去構成這些服務,把咱們鏈接在一塊兒的是咱們解釋這些樸素的字符串的方式,咱們把它們稱爲協議

WSGI一樣是一個協議,WSGI最大的優點是,全部實現WSGI接口的應用都可以運行在WSGI server上。經過這種方式,實現了Python WSGI應用的可移植。Django和Flask的程序能夠混編在一塊兒,在一個環境上運行。在我實現的框架Lunar中,使用了多種WSGI server進行測試。

在一些文章中,把相似於router,template engine等組件,包裝在網絡框架之中,WSGI應用之上的這些組件成爲WSGI中間件,得益於WSGI接口的簡單,編寫WSGI中間件變得十分簡單。在這裏,最難的問題是如何處理各個模塊的解耦。

考慮以前提到的模板引擎和ORM framework的實現,模板引擎和數據庫ORM都須要獲取應用的上下文(context),這是實現整個框架的難點。也是項目將來重構的核心問題。

如今代碼之爛是讓我沒法忍受的。最近開始讀兩本書,代碼整潔之道重構,本身在處理大型的軟件體系,處理不少設計模式的問題的時候仍是很弱逼。首先會拿模板引擎開刀,我有一個大致重構的方案,會很快改出來,力爭去掉parser中的大條件判斷,而且嘗試作一些性能上的優化。

Lunar是一個學習過程當中的實驗品,這麼無聊,老是要寫一些代碼的,省得畢業後再失了業。

在最後,仍是要感謝亮叔https://github.com/skyline75489,亮叔是我很是崇拜的一個Pythoner,或者說coder,一名天生的軟件工匠。沒有他這個項目不會有這麼高的完整度,他改變了我對這個項目的態度。

相關文章
相關標籤/搜索