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,一名天生的软件工匠。没有他这个项目不会有这么高的完整度,他改变了我对这个项目的态度。

相关文章
相关标签/搜索