python语言里有【装饰器】这么个语法糖。简单地理解就是,它能够神不知鬼不觉地替换掉被装饰的函数。【李代桃僵】之类的形容词当然不是描述装饰器的,而似乎装饰器也很少在代码中使用。不过,最近hack别人的python代码(具体来说,就是hack强大的calibre软件的recipe处理,添加cache功能),发现装饰器的设计本意【处理函数的执行环境】十分的有用。

以django项目中的某个功能为例,网址http://py.banjuan.net/tool/build_recipe/对应django工程中的某个函数 tool.build_recipe(request),这个网址GET的时候吐出网页,而POST的处理结果以json方式返回。那么我们就可以编写两个装饰器来专门处理这种页面渲染和json转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.http import HttpResponse
from django.utils import simplejson
from django.shortcuts import render_to_response

def page_render(view):
    def func(request, *arg):
        (tpl, ret) = view(request, *arg)
        return render_to_response(tpl, ret)
    return func

def json_render(view):
    def func(request, *arg):
        pydata = view(request, *arg)
        json = simplejson.dumps(pydata, ensure_ascii=False)
        return HttpResponse(json)
    return func

如果需要装饰,那么就是这样写:

1
2
3
@page_render
def build_recipe(request):
    return "running build_recipe"

给略懂python额外讲解一下,上面的代码定义了两个函数page_render, json_render,函数的返回值还是函数。当这两个*_render(其实就是所谓的装饰器了,和普通函数没啥区别)用来装饰build_recipe(request)时,就会起到【李代桃僵】的左右,实际执行的就只是func(xxxx)了。被装饰的原函数build_recipe(request)会作为参数传递给page_render。形象地理解,上面那句@page_render会等效于:

1
build_recipe = page_render(build_recipe)

实际上,我的代码中是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def build_recipe(request):
    #@csrf_protect
    @page_render
    def get(request):
        return ("tool/build_recipe.html", {})

    @json_render
    def post(request):
        content = None
        if 'content' in request.POST:
            content = request.POST['content']

        if 'recipe' in request.FILES:
            f = request.FILES['recipe']
            content = f.read()

        ret = 1000
        if content:
            name = mktemp(prefix=u'ebook-', suffix='.recipe')
            with open(name, 'wb') as f:
                f.write(content)
                f.close()
            cmd = EBOOK_CONVERT%(name, name+u'.mobi')
            #ret = os.system(cmd)
            ret = cmd
        return {'ret': ret}

    if request.method == 'POST':
        return post(request)
    else:
        return get(request)

根据request.method来分流处理,get()/post()只是返回基本数据,然后交由各自的装饰器进行数据渲染(弄成网页,变为json等)。于是上面的page_render/json_render便可以大量应用与其他类型的函数上,节省代码。

而在实际项目中,除了页面渲染,一个网页请求可能需要先验证登陆态、然后校验典型参数的有效性,为了调试还得统一捕抓异常,为了对付各种黑客还需要csrf_token(就是上面那行注释掉的装饰器),返回结果也还要统一,等等。这些公共的操作都是可以使用装饰器来实现。根据不同的页面权限,组合使用不同的装饰器(会从上倒下层层装饰),达到简化工作的目的。

当然,装饰器的另外一个作用技巧是用来实现注册功能,能够【显得】简约、整洁许多:

1
2
3
4
5
6
7
8
9
10
11
12
funcs = {}
def register(func):
    funcs[func.func_name] = func
    return func

@register
def abc(msg): print msg

@register
def show(): print "hello"

print len(funcs), " functions registed"

最后,果然觉得python的诡异细节真是无穷尽啊。


啊,补充说一下。最初想要用装饰器,是因为calibre的处理recipre的代码中,下载url的start_fetch(url)被调用得太过凌乱了,手动修改各处调用来增加cache会很崩溃的。于是就想到了【李代桃僵】的装饰器的特性,虽然其他地方仍然是调用start_fetch(url),但是已经被我神不知鬼不觉地掉包了,HACK成功!或许有人会想,为啥不直接修改start_fetch(url)的内部代码??介个嘛,这要是因为start_fetch(url)内处的处理也很冗长凌乱,改起来也很崩溃……所以,还是【装饰器】好用啊!

, , ,

手头的项目是使用DJango开发的,不过因为需要处理大量的文本,所以才用了jinja2作为部分配置文件生成的模板引擎。一般也就简单地使用:

1
2
3
from jinja2 import Template
t = Template( configure_tpl )
    return t.render(value=bala, value2=bala, ...)

今天突然需要使用jinja2中的filter(过滤器,理解为函数),于是跑去翻jinja2的文档。看了大段大段的,依旧一头雾水。神马Environment,Loader,get_template(),我根本就不需要这么复杂的功能嘛!而文档里的举例也是只言片语,常常就说这样可以添加filter:

1
2
3
def my_filter(value):
    return bala(value)
env.filters['my_filter'] = my_filter

可是,这根本就不说env变量如何弄来的!!!而且又如何生成我的configure_tpl文件?都是木有上下文的,要自己去猜……

后来索性去翻看jinja2库里对Template的实现,终于发现一个特别有用的函数:env.from_string(),顿时泪流满面……

所以,其实添加filter的最简单、完整的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python
#-*- coding: UTF-8 -*-
# 替代原来的 jinja2.Template(tpl).reander()
import jinja2
def my_filter(value):
    return unicode(value) + "_nice"

env = jinja2.Environment()
env.filters['my_filter'] = my_filter
tpl = "{{ val|my_filter }}"
template = env.from_string(tpl)
print template.render(val="hello")

接着来外番,讨论一下REST式的模板语言。还是针对jinja2库,以前生成出一个配置是需要一段较长的代码来查询数据库的内容的,然后保存为一个字符串丢给jinja2替换模板。这样导致每个配置文件都需要写各种代码,而且这些个代码还是类似却不相同。而其实套用流行的思想,这些配置文件与数据库的数据大多是一一对应的,可以认为是数据资源的存储、表现形式不一样而已。因此其实可以使用较为通用的代码/函数来封装这功能:

1
data = get_workers("china", "beijing", "pku")

同时还要将查询出来的数据弄成各种格式:

1
value = to_json(data)

表现在jinja2模板里,如果使用过滤器来实现的话,就是这样:

1
{{ database|get_country:"china"|get_city:"beijing"|get_school:"pku"|to_json }}

漫长的链式。从代码封装上来看,已经达到我的目标了。不过还需要考虑一个实际的问题:上面这样的模板会有各种人来更改,既有懂行的老同学,也有新来的实习生。上面这种乱乱的代码是很容易就导致错误的。而如果要封装得简约简单,那么首推就是REST风格了:

1
{{ database.china.bejing.pku.to_json() }}

对比前一种,整齐顺眼N倍,可维护性也高了很多。

REST风格的模板变量的实现,利用的是python多数模板库的一个常见的处理流程:所有的 value.abc 取值的时候,通常会变为对value取属性值abc。因此,实现__getattr__(self, key)方法,就能够捕抓这种属性值的访问(当然这里捕抓的是abc这种”本不存在”的属性),记录下来,然后在某个时候进行实际的查询处理。(这个也算是附带的【延时】操作效果)

示例代码放在了github上rest-template.py。可能需要注意的是,有可能REST路径中有纯数字的,例如

1
{{ phones.china.10086.email }}

对于纯数字,它是不可能为属性的,因此模板库会调用__getitem__(self,key)来查询,所以也需要写这个方法。

PS:REST风格的python函数库也有许多,典型代表则是BeautifulSoup库,它对各种tag的读取都是REST风格的。

,