巫女的新年

虽然拟了一个貌似相当格林童话式的标题,但事实上本文和三次元中居于鸟居之后巫女桑们没有关系,当然与二次元中的巫女小姐们也没有关系。标题中的巫女,是指miko这个项目(AS备注:miko是日文中巫女的发音,因为写这玩意的时候正在听那首著名的Neko Miko Reimu)。新年是一个蹩脚的隐喻,我个人使用这个来代指重构 —- 当然和时值新年也有莫大的关系。

于是您看出来了,这篇文章是一篇大概相当无聊的笔记……

不过不管怎么说,在此,AS祝您新年快乐。Happy New Year。あけましておめでとう。

miko并不是一个新坑,虽然我很喜欢开新坑……miko的前身是ShoujoA,也就是这个博客的生成器。虽然她并不是我用时最长的项目,也不是我耗费心力最多的项目,但确实是我最喜欢的项目。ShoujoA的写作时间是去年六月,陆陆续续用了八个月,算是一个基本能用的博客生成器。不过毕竟相隔了八个月,站在现在的角度,当时的代码质量确实很糟。尤其是,在上个月准备对博客进行搬迁的时候,发现了当时写作时候的一个疏漏导致无法指定时间(比如这篇文章是2014-02-04:22点写的,以前不能这样指定时间) —- 原来的方案只是简单获取了文件的修改时间。是的,我可以给她打一个补丁,修改几行,让她能像我期望的那样工作。但是混乱的代码,让我感觉很累,于是就重构了。

说是重构,毋宁说是直接重写了一遍 —- 这也是我更改项目名称的原因。

回过头看,之前的实现真是相当笨拙呢。(笑

miko相对与ShoujoA代码写作风格上有若干改进(如果算的话:

  1. 分散的多文件,尽力让单文件的行数在两百行以下,更容易修改和阅读

  2. 简化了Node类,去掉了滥用的@properity的属性。删掉了用于封装site配置的Site类

  3. 借鉴了liquidluck的源文件写作格式,使源文件更优雅

  4. 大量使用装饰器,从而实现了函数之间的松耦合

  5. 尝试使用函数式编程的一些关键字来处理列表

  6. 去掉了并不实用的insert方法,以及备份功能

  7. 在jinja2中大量使用filter

  8. 用一个简单漂亮的分页类代替了原来的分页代码

恩,其他的就算了,我主要想讨论的是2,4,5条。7的话,以前就写过了。

先说简化Node这个问题。

我不知道我在写ShoujoA的时候是不是受了某些什么东西的影响,挺多东西很刻意,以Node类为甚。Node类其实干的事情很简单 —- 解析器把解析出来的元素扔到Node类中,供后续引用,或者序列化。

但是以前我的写法是这样。

class Node(object):
    def __init__(*args,**kwargs):
        try:
            self._title = kwargs['title']
            ...
        except KeyError:
            ...
    @property
    def Title(self):
        return self._title
    ...

我猜我当时考虑的是类属性的pulic和private的问题……但是仔细想想,这样的设计纯属没用且蛋疼,先不说压根没有private属性使用的必要,引入@property之后,本来希望private的属性到头来还是public了,意义何在?

现在的话,直接省去了几乎所有的@property方法,而把所有的属性全部暴露出来,直接node.title这样引用不是蛮好?

@property的真正意义在于,把一个方法看作一个属性。一个更直白的表达是,让您少写一对括号。

所以,现在我会这样用:

class Node(object):
    def __init__(*args,**kwargs):
        try:
            self.title = kwargs['title']
            self._tags = kwargs['tags']
            ...
        except KeyError:
            ...
    @property
    def tags(self):
        taglist = self._tags.replace(u',',',').split(',')
        return [tag.strip() for tag in taglist] 

    ...

当然,tags可以在外面解析完直接传进来,但是这样不仅不自然,而且有重复代码的可能性。直接扔进去,当作属性来引用是一种优雅的方式。

其实推广开的话,我也考虑过整个parser也都放在node里面,作为一个方法,但是后来没有。原因只是单纯的不希望node太臃肿。

我甚至考虑过去掉初始化函数,直接使用python的动态插拔的特性,但是这样局限性很严重,而且无法引入__lt__之类的魔术方法(magic method)来简化排序的代码,所以也就放弃了这种想法。

Site之前是和Node类一样的写法,只不过传入的全是解析出来的site的配置字典。Site封装了所有的配置,以便在jinja2中通过类似于{{site.title}}这样的方法进行引用配置。

但是更深入地研究了jinja2后,我觉得这种方法很傻……

考虑到解析出来的配置文件已经是一个字典了,我们可以直接把这个字典作为jinja2的初始的全局变量env.globals=configs,这样就可以直截了当地使用{{title}}这样的方式引用值。不仅省去了多余的Site封装,而且写法更直接,render时传入的数据更少。

当然这需要理解jinja2的Environment。彼时的我,只把这东西当作用来得到get_template,render等方法的一个包而已……

再说装饰器。

miko的一个显著的改进是大量使用(滥用)装饰器。

比如下面这个post函数:

@feed
@sitemap
@tags
@archive
@render
@update_theme
@save_nodes
@sort_nodes
@get_nodes
def post(nodes,filename):
    '''post a article,elements is a dict'''
    node = _node(filename)
    nodes = filter(lambda o:o.title != node.title,nodes)
    nodes.append(node)
    #_generatorHtml(nodes)
    return nodes

作为miko中最重要的一个函数,post本身的代码量相当少,主体的工作都在装饰器中完成。不过很明显的是,不仅post很容易懂,每个装饰器的用途也都很容易懂 —- 一个函数越单纯越好。

装饰器入门方面的东西其实有不少不错的资料,我也就不班门弄斧了,不过有一点使用经验是,最好所有的装饰器都返回同样的值来保证同一性(我瞎掰的词) —- 这样的说法不太精确,用上面的例子就是,上面所有的装饰器,返回都是nodes,这样可以使装饰器顺序排列稍微自由一些。

另外就是,谨慎处理None值,个人感觉直接抛出是个不错的手法,如果非要传递下去的话,确保下面的装饰器做好了准备。

装饰器流(上面那种)其实并不是推荐的使用方法,一般两个装饰器就相当合适。如果希望使用装饰器流的话,就需要需要谨慎地排列装饰器的顺序,以免一些不必要的错误。

装饰器毕竟是一个用来包装函数的函数,不能直接执行(比如说上面的update_themes,就不能update_theme()这样执行)。如果这个装饰器所执行的操作又希望直接执行的话,可以写一个空函数然后用装饰器包装。同样用update_themes这个函数作为例子:

@update_themes
def themes():
    return None

我以前认为,既然装饰器是用来装饰的,那么就不应该修改函数中的值,也不应该向函数中传值。但是这样,多没劲。

装饰器的本质是执行被包装的函数,并返回一个新函数。岂止是不修改函数中的值,压根就是直接替换了整个函数好伐?

下面的这个get_nodes装饰器个人就觉得很好玩。这个函数修改了被装饰的那个函数,强行赋值:

def get_nodes(func):
    def wrapper(*args,**kwargs):
        try:
            pick = open('data.pick','rb')
        except IOError:
            nodes = []
        else:
            nodes = pickle.load(pick)
            pick.close()
        return func(nodes,*args,**kwargs)
    return wrapper

@get_nodes
def post(nodes,filename):
    print nodes

post('xxx.xxx')

有些味道了么?

装饰器可以含參,lolibot那里面就使用了含參的装饰器,异常优雅。不过原理很简单,含參装饰器闭包中的函数是一个高阶函数(装饰器),如此而已。

def add_callback(type):
    def decorator(func):
    ...
    return decorator

@add_callback('text')
def text(massage):
    ...
    return xxx

最后是大量引入函数式编程的关键字。

其实老实说也并不是不能用一般的写法代替,不过函数式编程的手法处理序列有独特的优越性,而且能把代码写得更简单。

miko中应用得最多的关键字是map和filter,reduce没找到应用场景……

map对一个序列中所有元素挨个使用一个方法,然后返回结果列表。filter过滤掉不符合的数据。嘛,这些都很简单。

如果方法的参数并不是一个呢?

比如,postDir的参数有两个,虽然在处理一个序列的时候,第二个参数不变。

这时候,使用lambda就好了。map(lambda o:postDir(o,constant),objects)。很轻松愉快不是么?

受限于miko本身,其实miko中函数式编程的痕迹并不明显。虽然我很想使用闭包去模拟类,但是毕竟没有对应的场景,感觉好遗憾……orz。

基本上,miko的新改进就是这样,这一版代码,个人挺满意的,如果有一天又需要重写miko,我很好奇能写成什么样……

然后针对博客,我也做了一点优化。恩,虽然您应该不会注意到,但是现在博客的载入速度变快了大概0.5秒……优化其实很单纯,就是把所有的js,css以及png,全部放在七牛盘上了(我不是做广告,单纯只是七牛盘干净而且支持外链)。由于七牛盘在国内,所以这部分载入时间相当少。虽然七牛的体验用户并不是很大方,但是毕竟只是js,css的话,流量也很少。允许的月pv10w对我这个小博客来说更是遥不可及。

然后我在服务器上跑了一个dropbox用来同步,写了一个定时脚本,每天11点半工作三分钟。就效果而言,相当满意。

以上就是这次关于miko的一些东西。

看到这里的您辛苦了,新年快乐!

以上。

由于过年恐惧症而郁郁寡欢的AS。