About

Redis Cluster 简单配置与动态扩容

    Redis 3.0 就要自带集群功能了, 去看了一下这里还有官方教程之后, 发现似乎必须用命令行来搞着, 而且官方提供的 redis-trib.rb 要求至少 3 个节点才能建立一个集群, 这规格是向党支部看齐么...
    至少 3 个节点这个还是略坑, 而且不能自动添加节点 (难道要我启动个 py 的 subprocess 去掉 ruby?), 于是去看看源代码, 惊讶地发现, 原来限制 3 个节点起步的是 ruby 脚本, 而且调集群加节点平衡负载其实都可以用 redis 命令来完成. 好吧, 那我自己来连 socket 搞总行了吧.
    结果一番折腾还真的可行的样子, 于是有了这篇文章和一个简单的工具. 那么首先说说怎么用 redis-cli 来做这些事情.

    如何在 redis-cli 手动启动集群呢, 请随意连上一个空的支持集群模式的节点, 然后执行
cluster addslots 0 1 2 ... 16383
    千万不要误会了, 中间那个 ... 可是要实打实地从头写到尾的哦. 所以如果可以的话, 手动写个脚本来干这事情吧.
    不过也可以略过这些步骤, 反正下面看看例子就行, 最后会给出一个 Python 工具来做这些.
    接下来的例子中, 假定已经开好了一个集群, 共有 3 个 master 节点. 要在控制台检视这些节点, 请用 redis-cli 随意连上其中一个, 并执行
cluster nodes
    输出
e7f4fcc0dd003fc107333a4132a471ad306d5513 127.0.0.1:8001 master - 0 1414033928009 3 connected 0-2729 8192-10921
bd239f7dbeaba9541586a708484cdce0ca99aba5 127.0.0.1:8000 master - 0 1414033929011 2 connected 2730-8191
787e06e9d96e6a9a3d02c7f3ec14e243882293e9 127.0.0.1:7999 myself,master - 0 0 1 connected 10922-16383
以上每一行是一个节点信息, 按空格分隔的域依次表示
  • 节点 ID
  • 节点地址
  • 节点角色 (master / slave), 如果是当前节点, 还会有个 myself
  • 对于 slave 而言, 其 master 节点的 ID
  • 最后一次 ping 时间戳
  • 最后一次 pong 时间戳
  • 节点顺序号
  • 节点连接状态
  • 之后的所有 : 节点所配给的槽位, 如果槽位连续, 就以 BEGIN-END 表示, 不连续的由空格隔开
    如果要向集群新增一个节点, 需要用 redis-cli 连上这个新节点, 调用一次 cluster meet 命令. 如
cluster meet 127.0.0.1 7999
后面参数是已经在集群中的节点中任意一个节点的地址及端口. 然后再来一次

Permanent Link: /p/524/

Post tags:

Python

Redis

集群

麻将听牌算法 [下篇]

上篇中分析了听牌可能有关字牌的情形, 具体包括字牌中有一个单张, 而剩下的数牌全能构成面子的单骑醒, 或者字牌中有个对子, 而剩下某数牌含有一个对子的双碰型或一个搭子的边/嵌张听牌. 这篇要讨论字牌全是刻子时的类似情况. 之所以说类似是由于此时数牌只可能有以下两种情况
  • 某一色数牌的牌总数模 3 余 1, 其它两个色都能恰好构成面子
  • 某两色数牌的牌总数摸 3 余 2, 剩下一色能恰好构成面子
体现成代码就是, 需要解决以下两个函数
def _waits_4groups(tiles):
    # 前略
    # 在前面情况不满足时, 调用如下实现
    return (_detect_numeric_suit_with_one_more(tiles) +
            _detect_2_numeric_suits_with_2_more(tiles))

# 找一个花色, 它的数量模 3 余 1
def _detect_numeric_suit_with_one_more(tiles):
    pass

# 找两个花色, 它们各自的牌的数量模 3 都余 2
def _detect_2_numeric_suits_with_2_more(tiles):
    pass
在上一篇代码的支援下, 后一个函数的实现相对容易一些, 如下

Permanent Link: /p/522/

Post tags:

Algorithm

Python

麻将

麻将听牌算法 [上篇]

    作为一个人类经常在打清一色的时候望着手牌不知道听牌没不知道听了哪几张也不知道切哪一张会让听牌数量最大化是一件不愉快的事情, 除了九莲宝灯之类的定式役给背下来好像没别的有效方法. 或者, 写个程序来搞吧.
    首先是数据结构, 这里用如下类来描述

Permanent Link: /p/521/

Post tags:

Algorithm

Python

麻将

就算是 Linux 命令行只要有爱就能剪辑 MAD 了吧

起因当然就是, UP 主以前还没当过 UP 主呢, 这会儿想剪个 MAD 啦.

经过是, 写了个简单脚本用 avconv + mencoder 剪出没有任何特效, 只是纯粹拼接原始片段的视频.

教训是 Linux 从来不缺乏图形界面软件, 只是... 缺乏比对应的命令行软件更优秀的图形界面软件.

这次实践, 基本原理是用 avconv (不要吐槽名字啊, 其实这软件很健全的) 对源视频进行分割, 然后用 mencoder 串联起来, 再配上音乐.

Ubuntu 上安装这些东西以及对应的转码器

apt-get install libav-tools libavcodec-extra-53 mencoder

(Windows 上也有 avconv 和 mencoder 和, 理论上也能这么搞)

直接运用上述工具, 跟一般 Linux 命令行软件一样的问题就是, 参数略复杂, 比如用 mencoder 合并视频

mencoder -ovc copy -o OUTPUT_FILE.mp4 INPUT_FILE INPUT_FILE INPUT_FILE ...

而用 avconv 从一个视频源文件中提取一段内容, 转压成 640x360 分辨率的 MP4 文件, 去掉声音, 要这么干

avconv -ss 00:00:10 -i INPUT_VIDEO_FILE -t 00:00:30 -vf "scale=640:360" -f mp4 -vcodec libx264 -an OUTPUT.mp4

上面 -ss 参数后面是开始时间, -t 参数后是剪出的片段长度. (因为视频流压缩的问题, 这两个时间都可能不精确)

对于制 MAD 来说一次要剪出上百个片段, 这么一个个手打再多的爱最后也是死路一条. 所以得换个方式, 选取一些相对容易的工具; 当然如果没有, 就造一个.

这个轮子构想的出发点是尽量简化对视频剪裁参数的编写. 因为制作 MAD 往往是从多个视频中混合剪出片段, 而且顺序不确定, 所以填写文件名还是太麻烦, 可以这么考虑

将所有源视频放在一个目录下, 文件名前面编号 (如果是同一部动画, 那么就是集数了)

由一个文件给出剪取顺序, 信息包括视频顺序编号, 场景开始时间, 场景长度; 为了让这个文件更容易维护, 也允许文件中有注释

剩下的参数只需要指定输入的音频文件路径和输出的最终文件路径即可

中间弄出来的片段都放临时文件目录好了

基于以上指导思想, 实作如下的 Python 函数

Permanent Link: /p/518/

Post tags:

Python

视频剪辑

索引统计与 Python 字典

    最近折腾索引引擎以及数据统计方面的工作比较多, 与 Python 字典频繁打交道, 至此整理一份此方面 API 的用法与坑法备案.

    索引引擎的基本工作原理便是倒排索引, 即将一个文档所包含的文字反过来映射至文档; 这方面算法并没有太多花样可言, 为了增加效率, 索引数据尽可往内存里面搬, 此法可效王献之习书法之势, 只要把十八台机器内存全部塞满, 那么基本也就功成名就了. 而基本思路举个简单例子, 现在有以下文档 (分词已经完成) 以及其包含的关键词
  • doc_a: [word_w, word_x, word_y]
  • doc_b: [word_x, word_z]
  • doc_c: [word_y]
将其变换为
  • word_w -> [doc_a]
  • word_x -> [doc_a, doc_b]
  • word_y -> [doc_a, doc_c]
  • word_z -> [doc_b]
    写成 Python 代码, 便是
doc_a = {'id': 'a', 'words': ['word_w', 'word_x', 'word_y']}
doc_b = {'id': 'b', 'words': ['word_x', 'word_z']}
doc_c = {'id': 'c', 'words': ['word_y']}

docs = [doc_a, doc_b, doc_c]
indices = dict()

for doc in docs:
    for word in doc['words']:
        if word not in indices:
            indices[word] = []
        indices[word].append(doc['id'])

print indices
    不过这里有个小技巧, 就是对于判断当前词是否已经在索引字典里的分支
if word not in indices:
    indices[word] = []
可以被 dictsetdefault(key, default=None) 接口替换. 此接口的作用是, 如果 key 在字典里, 那么好说, 拿出对应的值来; 否则, 新建此 key, 且设置默认对应值为 default. 但从设计上来说, 我不明白为何 default 有个默认值 None, 看起来并无多大意义, 如果确要使用此接口, 大体都会自带默认值吧, 如下
for doc in docs:
    for word in doc['words']:
        indices.setdefault(word, []).append(doc['id'])
    这样就省掉分支了, 代码看起来少很多.
    不过在某些情况下, setdefault 用起来并不顺手: 当 default 值构造很复杂时, 或产生 default 值有副作用时, 以及一个之后会说到的情况; 前两种情况一言以蔽之, 就是 setdefault 不适用于 default 需要惰性求值的场景. 换言之, 为了兼顾这种需求, setdefault 可能会设计成
def setdefault(self, key, default_factory):
    if key not in self:
        self[key] = default_factory()
    return self[key]
倘若真如此, 那么上面的代码应改成
for doc in docs:
    for word in doc['words']:
        indices.setdefault(word, list).append(doc['id'])

Permanent Link: /p/517/

Post tags:

Data Structure

Python

简易配置 gunicorn

引子

    单纯 gevent 跟 nodejs 一样有个问题是如果服务器有大的同步计算 (比如压缩一张图片什么的) 需求时, 服务器会很卡. 这也不能怪它们, 因为本来它们的长处是 IO 异步化, 同步计算卡住是缺陷特性之一.
    然, 或荐基独搅受 gunicorn 以解此困. 只是其首页上例子意味不明, 各种文档文章都说要编写一些离奇复杂的配置文件, 然后跑个语焉不详的 hello world, 并没能明示重点问题.

正文

    嘛, 一番探索之后配了下面一个用例 (Flask)
import time
import flask

app = flask.Flask(__name__)

@app.route('/
')
def root(n):
    time.sleep(2)
    i = n / 2
    while 1 < i:
        if n % i == 0:
            return 'not prime'
        i -= 1
    return 'prime'

if __name__ == '__main__':
    app.run(port=8000)
    这个例子里面兼顾了长 IO (用睡眠去模拟) 跟大计算 (算请求的数是不是个素数). 把这货在控制台裸着启动起来, 然后用 apache benchmark 来一发 (如果觉得后面请求参数里那个素数不够大, 可以自行算一个大的替换)
ab -n 500 -c 50 localhost:8000/16785407
    当然了, -c 50 这个参数纯是卖萌的, 因为上面这代码自身根本异步不起来. 结果自然是惨不忍睹, 重点两行在测试机上表现如下
Time per request:       131417.472 [ms] (mean)
Time per request:       2628.349 [ms] (mean, across all concurrent requests)
    平均单个请求耗时 2.6 秒以上, 其中 2 秒是睡过去的, 剩下 0.6 秒是计算. 也就是说 IO 时间与计算时间大概的比例是 3:1.

    安装 gunicorn 可以直接通过 pip 安装, 简单容易, 就不废话了. 下面上 gunicorn 平装版, 把上面的文件保存为 test.py, 在控制台中执行
gunicorn -w 4 test:app
    这个是说, 开 4 个进程跑 test 模块下的 app (就是文件里全局定义的 app 变量啦). 现在再开 ab 来一炮 (参数完全相同), 结果是
Time per request:       33150.026 [ms] (mean)
Time per request:       663.001 [ms] (mean, across all concurrent requests)
    从结果上来看差不多就是裸跑的 1/4 了, 因为开了 4 个进程一起搅嘛.

    虽然有 4 个进程睡睡醒醒轮番搞, 但没有异步 IO 的支持, 进程睡着就不干事了. 作为要榨干 worker 进程以及 CPU 使用率的系统管理员来说这可不能忍, 于是继续折腾个 gevent 进去好了, 两者互补, 相得益彰.
    不过用 gunicorn 就不需要在文件最开始打猴子补丁了, gunicorn 有个参数直接让 gevent 嵌入进程
gunicorn -w 4 -k gevent test:app
    再来一发 ab, 结果是
Time per request:       9724.214 [ms] (mean)
Time per request:       194.484 [ms] (mean, across all concurrent requests)
    嘛, 算是还看得过去的数据了.

补充说明

绑定其它端口

Permanent Link: /p/516/

Post tags:

Flask

Gevent

Gunicorn

Python

Web Server

记一些 (没) 有意义的 reduce 用法

    在 Python 或 Javascript 等许多语言中都有 reduce 函数. 其中 Python 中 reduce 作为全局函数出现, 而 Javascript 中则是 Array 的成员函数. 大量的用 reduce 来做累加累乘之类的例子就不说了, 这里探讨一个特殊的用例.
    前端经常会需要将页面中用户填写的一些内容打包成 JSON 字典, 比如一个注册页面片段







    Submit





document.getElementById('subm').onclick = function() {
    var inputValues = {
        email: document.getElementById('email').value,
        password: document.getElementById('password').value,
        address: document.getElementById('address').value,
        phonenum: document.getElementById('phonenum').value
    };
    /* process inputValues */
};


    以后每次这个表单多一项时, 构造 inputValues 时就会多一项, 代码维护会很烦.
    如果能这样写的话可能会好一些
var inputValues = {k: document.getElementById(k).value
                   for k in ['email', 'password', 'address', 'phonenum']};
    可惜 Javascript 里面没有温暖人心的 dict comprehension... 于是, 就有了下面这种 reduce 替代品 (终于正题了)
var inputValues = ['email', 'password', 'address', 'phonenum'].reduce(
    function(obj, item) {
        obj[item] = document.getElementById(item).value;
        return obj;
    }, {}));
    看来 Python 在语法灵活度上的优势已经虐掉 filter, map 函数两条街, 而且 dict comprehension 还能顺带打压一下 reduce, 也难怪 Guido 会挑明了说他不喜欢这些函数. 不过他不喜欢 reduce 主要是这东西太灵活, 一眼看不明白 (能一眼看明白的都直接用 sum 去了). 比如上面这个用例如果没有铺垫, 要理解还得废点草稿纸 (用来提升 bignity 倒是不错). 而下面这个例子也是 (Python)
some_dict = {
    'x': {
        'y': {
            'a': 10
        },
        'z': {
            'b': 20
        },
    },
}
reduce(lambda obj, key: obj.get(key, dict()), ['x', 'y', 'a'], some_dict)

Permanent Link: /p/515/

Post tags:

Javascript

Python

函数式程序设计

Flask / MongoDB 搭建简易图片服务器

前期准备

通过 pip 或 easy_install 安装了 pymongo 之后, 就能通过 Python 调教 mongodb 了.
接着安装个 flask 用来当 web 服务器.
当然 mongo 也是得安装的. 对于 Ubuntu 用户, 特别是使用 Server 12.04 的同学, 安装最新版要略费些周折, 具体说是

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt-get update
sudo apt-get install mongodb-10gen

如果你跟我一样觉得让通过上传文件名的后缀判别用户上传的什么文件完全是捏着山药当小黄瓜一样欺骗自己, 那么最好还准备个 Pillow 库

pip install Pillow

或 (更适合 Windows 用户)

easy_install Pillow

正片

Flask 文件上传

    Flask 官网上那个例子居然分了两截让人无从吐槽. 这里先弄个最简单的, 无论什么文件都先弄上来
import flask

app = flask.Flask(__name__)
app.debug = True

@app.route('/upload', methods=['POST'])
def upload():
    f = flask.request.files['uploaded_file']
    print f.read()
    return flask.redirect('/')

@app.route('/')
def index():
    return '''










    '''

if __name__ == '__main__':
    app.run(port=7777)
  • 注: 在 upload 函数中, 使用 flask.request.files[KEY] 获取上传文件对象, KEY 为页面 form 中 input 的 name 值
    因为是在后台输出内容, 所以测试最好拿纯文本文件来测.

保存到 mongodb

    如果不那么讲究的话, 最快速基本的存储方案里只需要
import pymongo
import bson.binary
from cStringIO import StringIO

app = flask.Flask(__name__)
app.debug = True
db = pymongo.MongoClient('localhost', 27017).test

def save_file(f):
    content = StringIO(f.read())
    db.files.save(dict(
        content=bson.binary.Binary(content.getvalue()),
    ))

@app.route('/upload', methods=['POST'])
def upload():
    f = flask.request.files['uploaded_file']
    save_file(f)
    return flask.redirect('/')
    把内容塞进一个 bson.binary.Binary 对象, 再把它扔进 mongodb 就可以了.
    现在试试再上传个什么文件, 在 mongo shell 中通过

db.files.find()

    就能看到了. 不过 content 这个域几乎肉眼无法分辨出什么东西, 即使是纯文本文件, mongo 也会显示为 Base64 编码.

提供文件访问

Permanent Link: /p/514/

Post tags:

Flask

MongoDB

Python

Tutorial

Web Server

PLY 构造词法分析工具

    PLY 需要安装一份. 可以直接通过 pip 安装

# pip install ply

    这东西并非一个扩展的正则表达式工具, 而是一个完备的编译器构造工具, 不过这篇文章只打算讨论其词法分析器构造部分.

基本例子

    PLY 很魔法的一点是它使用到了模块内部反射. 也就是说在产生一个词法分析器时, 并不是把词法规则传递给 PLY 的接口, 而是依次将一些指定名字的变量或函数定义在 py 文件中.
    下面给出第一个例子, 从文本中抓出十进制数值.
import ply.lex

tokens = (
    'NUMBER',
)

t_NUMBER = r'\d*[.]?\d+'

def t_error(t):
    t.lexer.skip(1)

ply.lex.lex()
ply.lex.input('''
    The Chinese mathematician Zu Chongzhi, around 480 AD, calculated that
        pi ~= 355/113
    (a fraction that goes by the name Milv in Chinese), using Liu Hui's
    algorithm applied to a 12288-sided polygon. With a correct value for its
    seven first decimal digits, this value of 3.141592920... remained the most
    accurate approximation of pi available for the next 800 years.



''')

for token in iter(ply.lex.token, None):
    print token.type, token.value
    输出

NUMBER 480
NUMBER 355
NUMBER 113
NUMBER 12288
NUMBER 3.141592920
NUMBER 800

    上面的代码有这么一些要点
  • 文件中定义了 tokens 表示可能的词元类型; 在官方例子中, 其中的取值通常以全大写的形式出现
  • 定义词元规则 t_NUMBER, 其名字是 token 变量中的成员 'NUMBER' 加上前缀 t_; 在构造词法分析器时, PLY 会将以 t_ 开头的所有定义收集起来
  • 词元规则 t_NUMBER 的取值是正则表达式, 用来匹配所有的数值
  • 定义 t_error 函数, 如果什么奇怪的东西混进来, 这个函数会被调用; 不过现在只是抓取数值, 无视其它符号, 所以实现只是跳过一个字符 (skip 的参数是字符数量)
  • 调用 ply.lex.lex 构造词法分析器
  • 调用 ply.lex.input 喂一些输入进去
  • ply.lex.token 获得词法分析的结果

利用分词输出

    从这个基本例子迈向一个用于简单表达式计算的词法分析器并不困难. 如果按照之前一篇文章里所演示的那个功能 (请将该文章中最后一段代码保存在 expr_eval.py 中以供这次使用), 需要至少以下这些词元类型
  • 二元算符 */^
  • 二元或一元算符 +-
  • 括号
  • 整数

Permanent Link: /p/513/

Post tags:

Compiler Construction

PLY

Python

Tutorial

多国语言环境下 Jinja2 与 strftime 互动异常退治纪要

    不少语言或者库或多或少都遭遇过乱码问题. 然而这一点在 Python 里面表现得比较另类, 一般来说只要贯彻 UTF-8 到底的思路, 不怎么可能遇到一大波乱码袭来的情况. 只是, 偶尔会直接因为字符串类型设置不对而直接抛运行期异常.
    常在河边走, 湿鞋是顺理成章的事情. 这次遇到的情况简报如下
# encoding=utf-8

import jinja2
import datetime

now = datetime.datetime.utcnow()

print jinja2.Template(u'''当前月份 {{ date.strftime('%Y 年 %m 月') }}''').render(date=now)
运行这一段代码, Python 直接扔出来一坨
Traceback (most recent call last):
  File "test.py", line 8, in

    print jinja2.Template(u'''当前月份 {{ date.strftime('%Y 年 %m 月') }}''').render(date=now)
  File "/usr/local/lib/python2.7/dist-packages/Jinja2-2.6-py2.7.egg/jinja2/environment.py", line 894, in render
    return self.environment.handle_exception(exc_info, True)
  File "

Permanent Link: /p/510/

Post tags:

Jinja2

Python

strftime

Unicode

Flask 出坑记

    Flask 是个 Python Web 框架. 网站上文档例子都很详尽, 这里就不废话了, 只是来扯两个使用中需要注意的地方.

获取 POST 请求体

    21 世纪的 Web 交互中服务器跟浏览器互相丢 JSON 已经成了司空见惯的事情. 在 Flask 框架作成的服务器上要搞到 JSON 数据当然是直接访问 POST 请求体了, 如
import flask
import functools

app = flask.Flask(__name__)

@app.route('/wtf', methods=['POST'])
def wtf():
    return 'Received: ' + flask.request.data

def main():
    app.run(port=7777)

if __name__ == '__main__': main()
    按文档的说法, flask.request.data 包含请求数据字符串. 但其实这也是个坑, 默认情况下根本取不到请求数据
$ curl -d "[1,1,2,3,5,8]" http://localhost:7777/wtf
Received:
    熊孩子你把拿到的字符串给吃了吧? 实际上如果去看看那文档会看到并不如上面所说的那样, 而是
  • Contains the incoming request data as string in case it came with a mimetype Flask does not handle.
    后面这个状语从句真是着急, 那到底什么 mimetype 会使得 Flask does not handle 呢? 根本没说清楚啊. 扫一眼文档后面, 还有个东西可以用: flask.request.json, 但这货一般是 None, 只有当请求 mimetype 被设置为 application/json 的时候才有用, Flask 你真心是跟 mimetype 过不去啊. 也就是说得这样发请求
$ curl -d "[1,1,2,3,5,8]" localhost:7777/wtf
Received: null
$ curl -d "[1,1,2,3,5,8]" -H "Content-Type:application/json" localhost:7777/wtf
Received: [1, 1, 2, 3, 5, 8]
# Server codes

import json
import flask
import functools

app = flask.Flask(__name__)

@app.route('/wtf', methods=['POST'])
def wtf():
    return 'Received: ' + json.dumps(flask.request.json)

def main():
    app.run(port=7777)

if __name__ == '__main__': main()
    问题是现在前端攻城狮都被浏览器兼容性折腾得满世界买表, 哪还有心情检查每个请求的 content-type 对不对. 况且这还只对 JSON 有效, 如果是山寨协议又怂了.
    好吧, 如果实在不行, 就挖到 WSGI 里面去好了, 比如这样
def request_data():
    d = flask.request.data
    if not d:
        return ''.join(flask.request.environ['wsgi.input'].readlines())
    return d

Permanent Link: /p/509/

Post tags:

Flask

Python

Web Server

移花接木 - 编译同步代码为异步代码 [条件表达式篇]

上篇

条件表达式

    如果设计如下的类生成 Javascript 中条件表达式 predicate ? consequence : alternative
# semantic.py

class Conditional(Expression):
    def __init__(self, predicate, consequence, alternative):
        self.predicate = predicate
        self.consequence = consequence
        self.alternative = alternative

    def compile(self, context):
        return output.Conditional(self.predicate.compile(context),
                                  self.consequence.compile(context),
                                  self.alternative.compile(context))
# output.py

class Conditional(Expression):
    def __init__(self, predicate, consequence, alternative):
        self.predicate = predicate
        self.consequence = consequence
        self.alternative = alternative

    def str(self):
        return '({p} ? {c} : {a})'.format(p=self.predicate.str(),
                                          c=self.consequence.str(),
                                          a=self.alternative.str())
    这在条件表达式的三个子表达式均不包含异步调用时, 或仅 predicate 包含正规异步调用没问题, 如
root_context = ContextFlow()
root_flow = root_context.block

Block([
        Arithmetics(
            Binary(
                '=',
                Reference('m'),
                Conditional(
                    Reference('a'),
                    Reference('b'),
                    Reference('c')))),
        Arithmetics(
            Binary(
                '=',
                Reference('n'),
                Conditional(
                    RegularAsyncCall(
                        Reference('f'),
                        [ NumericLiteral(0) ]),
                    Reference('x'),
                    Reference('y')))),
    ]).compile(root_context)

print root_flow.str()
    这些节点编译后会生成类似如下代码

Permanent Link: /p/508/

Post tags:

Asynchronous Expression

Compiler Construction

Javascript

Python

移花接木 - 编译同步代码为异步代码 [补述篇]

上篇

编译步骤解释

多个正规异步调用出现在同一个语句中的编译顺序

    RegularAsyncCall 对象的 compile 代码如下
def compile(self, context):
    # 0
    compl_callee = self.callee.compile(context)

    # 1
    compl_args = [ arg.compile(context) for arg in self.arguments ]

    # 2
    callback_body_context = ContextFlow()
    cb_body_flow = callback_body_context.block
    compl_args.append(output.Lambda([ 'error', 'result' ], cb_body_flow))

    # 3
    context.block.add(output.Arithmetics(output.Call(compl_callee, compl_args)))

    # 4
    context.block = cb_body_flow

    return output.Reference('result')
    步骤依次是
  • 0 编译被调用对象
  • 1 依次编译调用实参
  • 2 向实参列表中追加一个匿名函数, 函数体语句流是新建的一个 ContextFlow 中的语句流
  • 3 向当前上下文语句流中插入一个算术节点, 节点的内容是编译后的异步调用
  • 4 修改上下文语句流
    它们的顺序不可随意颠倒, 不仅仅因为有些步骤依赖于前面的步骤 (比如步骤 3 构造 Call 对象须 0 与 1 中得到的结果), 更多地是因为在编译过程中上下文语句流随时可能变化. 比如下面这个例子
async_call('xx', %%)('oo', %%)
这应当生成下面这段 Javascript 代码
async_call('xx', function(error, result) {
    result('oo', function(error, result) {
        result;
    });
});
    async_call('xx', %%) 编译时第一次修改了上下文语句流, 而之后对前一次异步调用结果的调用再一次修改了上下文语句流. 用语法树来表示即

+-------------+
| origin flow |
+-------------+
      |         +-------------+
      +---------| arithmetics |
                +-------------+
                       |
           [A]====================+
             | regular async call |
             +====================+
                 /               \
   [B]====================+    +------+
     | regular async call |    | 'oo' |
     +====================+    +------+
          /           \
   +------------+  +------+
   | async_call |  | 'xx' |
   +------------+  +------+

Permanent Link: /p/507/

Post tags:

Asynchronous Expression

Compiler Construction

Javascript

Python

移花接木 - 编译同步代码为异步代码 [数据结构篇]

目标

    我说 JS 代码的表达能力是一流的, 可能一票 Python 党 (作为高级黑, 我不得不说我自己也是个 Python 党) 要笑了. 我是说真的, Python 的 Lambda 基本上就是半残废, 所以, 要用 Python 写异步代码那就像是用挂面上吊一样不来劲了.
    然而任何写 JS 代码遇到三层以上的异步代码套在一起的时候手撕键盘的心都有了, 蔽博先前也介绍了一个工具 (确切地说是玩具) 用来使用类似同步的代码生成异步代码. 而现在则有更明确的目标, 如把这样一段代码
fs: require('fs')
content: fs.readFile('/etc/passwd', %%)
console.log(content)
变成这样一段 Javascript
var fs, content;
fs = require('fs');
fs.readFile('etc/passwd', function(err, result) {
    content = result;
    console.log(content);
});
    为什么是这样? 首先 JS 中很多异步调用的回调函数都遵循 function(error, result) 这个形式, 这俨然成为了 Javascript 中的一种约定, 因此在接下来的分析中把这样的回调函数参数称为正规异步回调 (regular asynchronous callback). 既然它们都是这么个形式, 那么写代码的时候完全可以不这么罗嗦写个回调声明, 而是用 %% 这个符号来替代它.
    而包含正规异步回调的函数调用称之为正规异步调用 (regular asynchronous call).

    更详细地说, fs.readFile('/etc/passwd', %%) 这个表达式的意思是
  • 调用 fs.readFile
  • 第一个参数是字符串常量 '/etc/passwd'
  • 第二个参数是一个正规异步回调函数
  • 从这个表达式之后, 所有的语句归入上述正规异步回调函数的函数体中
    那么下面要做的事请是, 如果在一次编译过程中, 语法分析已经完成, 对应的同步代码的语法树已经建起来, 怎么根据上述规则去生成异步代码.

    比如上面的例子, 源语法树形式是

Permanent Link: /p/506/

Post tags:

Asynchronous Expression

Compiler Construction

Javascript

Python

小记 Python yield 与生成器

    yield 是 Python 中的一个关键字, 这个关键字比较特殊, 用于在任何表达式前, 但它不仅会对其后的表达式有影响, 对整个函数上下文都有影响. 实际上, 凡是在函数体中出现了 yield 关键字, Python 都会对此函数特殊处理, 调用这个函数不再返回值, 而是一个生成器对象.
    比如
def f():
    yield 1

g = f()
print type(g)
其结果是


    如果要获取产生器产生的值, 则需要调用产生器对象的 next 函数
def f():
    yield 1

g = f()
print g.next()
    如果在函数中, 出现多个 yield, 或者函数的执行过程中反复路过某个 yield, 那么 next 每次调用会得到下一个产生的值, 比如
def f():
    yield 1
    yield 2

g = f()
print g.next(),
print g.next()
    得到的会是 1 2. 或如下的循环
def f():
    for x in range(3):
        yield x * 2

g = f()
print g.next(),
print g.next(),
print g.next()
    该循环中反复产生值, 每次产生一个就被传递给 next 函数作为返回值... 当然这样来说是不准确地, 如果上面 for 循环如果跑得太快, 那样会疾速产生值导致 next 函数应接不暇, 这样会有诡异的同步问题. 所以正确的语义应该是
  • 当生成器函数执行到包含 yield 的表达式时, 函数挂起, 并将 yield 之后的表达式作为返回值传递给 next 函数调用
  • 下一次 next 函数调用又会驱动该生成器的函数体继续执行此后的语句, 直到遇见下一个 yield 再次挂起并生成一个值交给 next
  • 如果某次 next 调用驱动了生成器继续执行, 而此后函数正常结束的话, 那么不会有任何值传递给 next 函数, 同时, 生成器会抛出 StopIteration 异常
    对于最后一点, 下面这个例子可作为参考
def f():
    yield 1

g = f()
print g.next(),
print g.next()
    另外生成器中如果出现 return, 它必须是一句空返回, 即 return 之后不允许跟任何表达式, 这也是限制之一.

    从下面这个例子可以更好地窥探生成器的执行模式
def f():
    print 'a'
    yield 1
    print 'b'
    yield 2
    print 'c'

g = f()
print 'start'
print g.next()
print 'next'
print g.next()
print 'end'
    输出结果为
start
a
1
next
b
2
end
    也就是说当第一次调用了 next 之后, f 函数体的执行就挂起了, 等着下一次调用 next. 另外, f 中最后一句应该输出的 c 并没有输出, 因为第二次生成器给出了 2 这个值之后就一直处于挂起状态.
    如果要大慈大悲地让最后一句也执行, 那么需要再加一句 next 调用
def f():
    print 'a'
    yield 1
    print 'b'
    yield 2
    print 'c'

g = f()
print 'start'
print g.next()
print 'next'
print g.next()
print 'end'
g.next()
    然而如果这样的话, c 输出之后, 还会带出个异常. 所以这东西有时候并不那么好用.

    那它有何用?
    一个典型的非它不可的场景是模拟无穷列表, 比如

Permanent Link: /p/502/

Post tags:

Generator

Python

yield

对象炼金术 - SQLAlchemy 使用 Postgres 数据库

安装 Postgres 与基本配置

Postgres, 或者又称为 PostgreSQL (结尾的小写 s 变成了 SQL) 是一个功能强大的数据库.
在 ubuntu 下安装
# apt-get install postgresql
其它发行版应该差不多. Windows 用户请自行解决.

安装完成之后, 系统中会多出一个名为 postgres 的用户, 这个用户用于登录数据库. 但无法直接用该用户在 shell 或 xdm 中登录, 须先用其它用户登录 shell, 然后 su 到该用户. 先为该用户设置一下密码
# passwd postgres
再切换到该用户
me@host:~$ su postgres
Password:
postgres@host:/home/me $
如果当时处在某个用户的 home 目录, 如 /home/me 下, 则命令行会弹出一行错误信息
could not change directory to "/home/me"
因为 postgres 这个用户无法读取当前用户的 home 目录. 觉得讨厌的话可以
$ cd /
此时在命令行输入指令进入 Postgres 交互环境
$ psql
psql (version number)
Type "help" for help.

postgres=# input instructions here
这样会以 postgres 用户身份登录, 如果要输入密码就输入刚才为 postgres 用户设置的密码.

这时 postgres 用户相当于数据库的根用户, 就像 root 用户之于 Linux 系统一样, 直接让应用程序使用 postgres 用户是比较危险的. 下面新建一个用户 "psuser", 密码为 "qwerty"
postgres=# CREATE USER psuser WITH PASSWORD 'qwerty';
CREATE ROLE
当然 Postgres 是不区分大小写的. 不过这里 (包括下文) 中将以全大写标明这些是关键字, 而小写的部分是可替换的. 密码需要用引号包围起来.

然后再建立一个新数据库, 并授权 psuser 这个用户可以使用该数据库
postgres=# CREATE DATABASE mydb;
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE mydb TO psuser;
GRANT
这样就差不多好了. 下面开始搞 Python 与 SQLAlchemy 部分.

如果上面每个句子输入之后没回显结果, 并且交互环境开头变为了 postgres-# (注意 # 前是一个减号而非等号), 请查看一下句尾的分号是否漏掉了.

使用 SQLAlchemy 连接 Postgres

有关 SQLAlchemy 的基本使用请见这里.
编写这样一段 Python 代码, 来测试 Postgres 数据库连接是否可用

Permanent Link: /p/493/

Post tags:

Postgres

Python

SQLAlchemy

使用 Python 2.7 作为 GAE 运行时

GAE 支持 Python 2.7 也有段时间了, 考虑到 2.7 加入了很多有意思的东西, 其中一些特性是先在 Python 3 里面出现, 然后移到 2.7 里面来的, 如
  • 有序字典 (而非默认的散列字典) 类型, 如 collections.OrderedDict({ 1: 'abc', 2: 'def' })
  • 集合类型字面常量, 如 { 1, 2, 3 } (以前版本写作 set([1, 2, 3]))
  • 字典及集合类型的推导式 (这么翻译准确么? Set / dictionary comprehensions), 如 { x: x * x for x in range(5) }
要简单地将运行时环境切换到 Python 2.7 版本, 需要修改 app.yaml 为如下形式
application: YOUR APP ID
version: YOUR APP VERSION
runtime: python27
threadsafe: false
api_version: 1
此外需要修改应用程序启动代码, 类似如下方式
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
import wsgiref.handlers
import webapp2

class Index(webapp.RequestHandler):
    def get(self):
        print 'Hello'

if __name__ == '__main__':
    application = webapp2.WSGIApplication([
        ('/', Index),
    ], debug=True)
    wsgiref.handlers.CGIHandler().run(application)
使用 2.7 版本的另一个好处是 GAE 上为 2.7 版本提供了很多库支持, 包括 jinja (这样就可以摆脱 django 模板了 :-). 如果需要引用 jinja2 库最近版本, 需要在 app.yaml 中加入以下内容
application: YOUR APP ID
version: YOUR APP VERSION
runtime: python27
threadsafe: false
api_version: 1

handlers:
- url: /.*
  script: main.py

libraries:
- name: jinja2
  version: latest
libraries 下面还可以附上其它想要使用的库. 在此处可以查看所有 Python 2.7 运行时所支持的库.

Permanent Link: /p/492/

Post tags:

Google AppEngine

Python

模板之工 - Mako 上手篇

简介

Mako 是一个 Python 模板程序. 用于给定一个字符串, 或给定一个文件, 使用 Python 对象值动态地替换其中的一部分内容.

可是使用 pip 或 easy_install 安装 Mako 库.
# pip install Mako
# easy_install Mako

使用

基本文本替换

安装完成后, 在交互环境中可以通过下面的代码来测试 Mako
>>> import mako.template
>>> print mako.template.Template('Hello, ${ what }!').render(what='Mako')
Hello, Mako!
上述代码中, 引入 Mako 模板模块, 并使用模板对字符串 Hello, ${ what }! 中的动态内容进行替换. 动态内容是形如 ${ what } 这样的 Mako 标记, 它表示将传递给 render 函数的字典中, what 对应的项的值替换这个标记. 因此上面的 render 结果便是 Hello, Mako!.

替换文件中的动态内容

更多的应用场景是将需要生成的内容写成一个文件模板 (如 HTML 页面), 读取这个页面并生成结果内容. 在当前目录下创建 hello-mako.html 文件, 内容如下



Dear ${ username }, welcome back!
并使用如下的 Python 代码
import mako.template
templ = mako.template.Template(filename='hello-mako.html')
print templ.render(username='Mako')
就能看到输出的结果 HTML 代码了.

使用中文及指定编码

上面的页面模板代码中如果包含中文或其它非 ASCII 字符, 如


你好, ${ username }, 欢迎回来!
那么这一段程序在构造 mako.template.Template 对象时多半会挂掉. 要为 Mako 模板指定输入编码方式才行
import mako.template
templ = mako.template.Template(filename='hello-mako.html', input_encoding='utf-8')
print templ.render(username=u'柑奈')

分支

接下来, 如果这个用户是个管理员用户, 那么希望为这个用户呈现一个后台管理的入口 (当然其它用户就没这个福利了), 那么可以用 Mako 提供的分支来实现
你好, ${ username }, 欢迎回来!

%if is_admin:
    管理页面
%endif
并使用如下语句分别测试
templ = mako.template.Template(filename='hello-mako.html', input_encoding='utf-8')
print templ.render(username=u'柑奈', is_admin=False)
print templ.render(username=u'柑奈', is_admin=True)
%if %endif 之间表示一段分支. 如果确实需要书写一个百分号, 而不是作为 Mako 的指令符号, 那么此处须连续两个百分号 %% 来转义, 通常页面上似乎也不会出现那么多百分号, 所以这个应该还是可以接受的吧.

关于分支, 更详细的例子是
%if condition_a:
    do_a
%elif condition_b:
    do_b
%else:
    do_c
%endif

对象式引用

Permanent Link: /p/491/

Post tags:

Mako

Python

Template

Tutorial

风之力 - Tornado 搭建基于 WebSocket 的聊天服务

    这年头 Python web 框架是有点泛滥了. 下面要介绍的是 facebook 的开源框架 tornado. 这东西比较简单, 而且自带 WebSocket 支持, 可以用它做个简单的聊天室.
    读者最好已具备 Javascript 与 WebSocket 的基础知识.

安装

    使用 easy_install 能很方便地爬到 tornado. 或者, 下载源代码, 解包后在源码目录执行
$ python setup.py build
# python setup.py install
即可.

开张

    首先还是来个 hello world.
import tornado.web
import tornado.ioloop

class Index(tornado.web.RequestHandler):
    def get(self):
        self.write('
Hello, world!')

if __name__ == '__main__':
    app = tornado.web.Application([
        ('/', Index),
    ])
    app.listen(8000)
    tornado.ioloop.IOLoop.instance().start()
    保存为 main.py, 然后执行
$ python main.py
    并访问 http://localhost:8000/ 即可看到页面中的 "Hello, world!".

    在分支中定义的 app 在构造时接受的一个列表参数
[
    ('/', Index),
]
用来配置 URL 映射, 比如这里访问根路径则映射至 Index 实例去处理, 在 Index 实例中, 定义的 get 方法将会处理请求.

处理 WebSocket 连接

添加请求处理类

    接下来就进入 WebSocket 环节. 先修改返回的页面, 让这个页面在加载后连接服务器.
class Index(tornado.web.RequestHandler):
    def get(self):
        self.write('''






        ''')
    修改这个类后, 然后在控制台中止服务器 (猛击 Ctrl-C), 并重新启动之.
    现在, 访问 http://localhost:8000/ 会遇到 404 错误, 因为 WebSocket 请求的 URL "ws://localhost:8000/soc" 还没有映射任何处理器, 因此这里需要再添加一个, 用于处理 WebSocket 请求的类.
import tornado.websocket

class SocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        self.write_message('Welcome to WebSocket')
    并为这个类加上 URL 映射
if __name__ == '__main__':
    app = tornado.web.Application([
        ('/', Index),
        ('/soc', SocketHandler),
    ])
    app.listen(8000)
    tornado.ioloop.IOLoop.instance().start()
    然后重启服务器, 并访问 http://localhost:8000/ 就可以在页面上看到服务器传来的信息了.

使用模板

Permanent Link: /p/489/

Post tags:

Python

Tornado

Tutorial

Web Server

对象炼金术 - SQLAlchemy 多外键关联同一实体类

    继续上节 SQLAlchemy 外键的探索.
    假设现在需求改变了, 需要给每个 Album 加个字段表示作词者, 而作词者本身也应是 Artist 的实例 (比如很多演唱者会自己为歌曲作词编曲等等), 这时 Album 会有超过一个关联到 Artist 的外键.
    如果仅仅简单如下处理
class Album(Base):
    __tablename__ = 'album'
    album_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)
    artist_id = sqla.Column('artist', sqla.ForeignKey('artist.id'))
    artist = sqlorm.relationship(
          Artist,
          backref=sqlorm.backref('albums', cascade='all,delete-orphan'))
    lyricist_id = sqla.Column('lyricist', sqla.ForeignKey('artist.id'))
    lyricist = sqlorm.relationship(
          Artist,
          backref=sqlorm.backref('written_albums', cascade='all,delete-orphan'))
然后添加一点数据进去
def save():
    artist0 = Artist(name='aki misawa')
    artist1 = Artist(name='katou emiri')
    artist2 = Artist(name='yamada hirosi')
    album0 = Album(name='stella musica', artist=artist0, lyricist=artist0)
    album1 = Album(name='hoshikage no ama no hara', artist=artist0,
                   lyricist=artist0)
    album2 = Album(name='jump!', artist=artist1, lyricist=artist2)
    session = Session()
    try:
        session.add(album0)
        session.add(album1)
        session.add(album2)
        session.flush()
        session.commit()
    finally:
        session.close()

if __name__ == '__main__':
    save()
结果运行立即悲剧, SQLAlchemy 会报下面的错误
sqlalchemy.exc.ArgumentError: Could not determine join condition between parent/child tables on relationship Album.artist. Specify a 'primaryjoin' expression.  If 'secondary' is present, 'secondaryjoin' is needed as well.
也许这是 SQLAlchemy 的问题, 明明这么明了的外键关系咋就跪了呢.

    好吧, 简单的正确答案是, 手动配置一下外键的 primaryjoin 属性, 如下

Permanent Link: /p/488/

Post tags:

ORM

Python

SQLAlchemy

Tutorial

对象炼金术 - SQLAlchemy 中关系的级联

    在上一节中谈到了如何把有关联的对象一起塞进数据表中, 现在来试试从数据表里面取出一条数据然后删掉.
    在之前代码的基础上添加下面的函数
def remove_album(name):
    session = Session()
    try:
        for a in session.query(Album).filter(Album.name == name).all():
            session.delete(a)
        session.flush()
        session.commit()
    finally:
        session.close()
    在这个函数中, 查询 Album 类型, 然后调用查询对象的 filter 函数, 指定列 name 的值严格等于该 name 参数. 接下来, 将查询得到的对象逐个删除.
    来看看搞起来如何
if __name__ == '__main__':
    save()
    remove_album('stella musica')
    list_all()
    嗯, 看起来还不错的样子.

    那么, 接下来试试删 Artist
def remove_artist(name):
    session = Session()
    try:
        for a in session.query(Artist).filter(Artist.name == name).all():
            session.delete(a)
        session.flush()
        session.commit()
    finally:
        session.close()

if __name__ == '__main__':
    save()
    remove_artist('katou emiri')
    list_all()
于是乎就悲剧了.
    崩盘之前的几行输出似乎是这样子的
= Albums =
stella musica
+ Artist: aki misawa
hoshikage no ama no hara
+ Artist: aki misawa
jump!
+ Artist:
    也就是说, remove_artist 确实从数据库中删去了名字为 'katou emiri' 的那个值, 因此名为 'jump!' 的那个 Album 实例的 artist 域成了 None, 悲剧就发生了.

    要解决这个问题, 就得在 AlbumArtist 的关联关系上动动手脚, 加上级联信息.

Permanent Link: /p/487/

Post tags:

ORM

Python

SQLAlchemy

Tutorial

对象炼金术 - SQLAlchemy 从外键到关系

上节回顾, 使用下面这样的代码保存一个 Artist 与一个 Album, 两条 flush 令人感觉相当不适.
def save():
    artist = Artist(name='aki misawa')
    session = Session()
    try:
        session.add(artist)
        session.flush() # 0
        album = Album(name='stella musica', artist_id=artist.artist_id)
        session.add(album)
        session.flush() # 1
        session.commit()
    finally:
        session.close()
    单纯一个外键还不能很直观地解决对象之间的关联关系. 下面就引入 sqlalchemy.orm.relationship 来改善这块代码.
import sqlalchemy as sqla
import sqlalchemy.orm as sqlorm
from sqlalchemy.ext.declarative import declarative_base as sqla_declarative_base

Base = sqla_declarative_base()
# use MEMORY, not a database file.
# and disable SQL echo
engine = sqla.create_engine('sqlite:///:memory:', echo=False)

class Artist(Base):
    __tablename__ = 'artist'
    artist_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)

class Album(Base):
    __tablename__ = 'album'
    album_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)
    artist_id = sqla.Column('artist', sqla.ForeignKey('artist.id'))

    artist = sqlorm.relationship(Artist)
    在类 ArtistAlbum 定义完成后, 再用 relationshipAlbum 补上一个类成员, 表示它与 Artist 之间的关系.

    这样一来, SQLAlchemy 就能根据此 relationship, 从对象关系中产生外键值, 比如之前的 save 函数可以变成这个样子
Session = sqlorm.scoped_session(sqlorm.sessionmaker(bind=engine))

def save():
    artist = Artist(name='aki misawa')
    album = Album(name='stella musica', artist=artist)
    session = Session()
    try:
        session.add(album)
        session.flush()
        session.commit()
    finally:
        session.close()

Permanent Link: /p/486/

Post tags:

ORM

Python

SQLAlchemy

Tutorial

对象炼金术 - 体验 SQLAlchemy

一直听说 SQLAlchemy 是个神一般的 ORM, 近期终于忍不住打算搞一搞, 还是小有收获的, 写一点出来, 欢迎大家来搞.
本文中的例子都是基于 Python 2.7, SQLAlchemy 0.7 的. 数据库使用 SQLite.

废话少说, 先来一段代码
import sqlalchemy as sqla
import sqlalchemy.orm as sqlorm
from sqlalchemy.ext.declarative import declarative_base as sqla_declarative_base

Base = sqla_declarative_base()
engine = sqla.create_engine('sqlite:///test.db', echo=True)

class Artist(Base):
    __tablename__ = 'artist'
    artist_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)

Base.metadata.bind = engine
Base.metadata.create_all()

Session = sqlorm.scoped_session(sqlorm.sessionmaker(bind=engine))

def save_artist():
    artist = Artist(name='aki misawa')
    session = Session()
    try:
        session.add(artist)
        session.flush()
        print 'Artist id:', artist.artist_id
        session.commit()
    finally:
        session.close()

if __name__ == '__main__':
    save_artist()
    运行这一段代码将会在当前目录创建 test.db 文件作为数据文件, 同时建立一个 Artist 表, 其中有主键字段 artist_id 跟表示名字的字符串列 name. 然后向这个表中插入一个对象, 并输出这个 artistartist_id
    在这一行
Base = sqla_declarative_base()
产生了 SQLAlchemy 中用于构造表的基类. 而接下来 create_engine 调用则绑定数据库以及文件, 并设置回显 SQL.

    然后重头来了, 声明类型 Artist
class Artist(Base):
    __tablename__ = 'artist'
    artist_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)
以及类成员 __tablename__ 表示表名, 接着以整数类型声明主键, 以及另一列 name 为字符串类型. 不用 varchar 真心舒畅.

    接着两句
Base.metadata.bind = engine
Base.metadata.create_all()
则是在数据库中建立表. 如果已经建立 (譬如第二次运行这个例子), 删去这两句似乎也没问题.
    虽然没看 SQLAlchemy 的实现, 但是构建 Base 类, 以及调用 create_all 建表有点多此一举的感觉. 我还是比较喜欢 GAE 存储那种直白的风格.

    后面的
Session = sqlorm.scoped_session(sqlorm.sessionmaker(bind=engine))

Permanent Link: /p/485/

Post tags:

ORM

Python

SQLAlchemy

Tutorial

基于 B/S 的桌游设计 - 游戏状态控制

三国杀结算模型

    考虑在游戏过程中, 某君使用万箭齐发, 对司马懿造成一点伤害, 司马懿发动反馈, 此时游戏暂停, 等待司马懿选择卡牌区域 (手牌或装备). 而在司马懿选择了反馈的卡牌区域后, 后续的玩家需要继续响应万箭齐发. 在万箭齐发结算完毕后, 回到某君的出牌阶段继续出牌或选择弃牌.
    好吧, 仔细看一下, 这就是个栈.
    题外话, 三国杀对延迟锦囊的判定顺序也是个栈, 后来的先判定.
    现在的问题就是怎么来表示这个游戏状态栈.

栈帧

    天下的栈大抵都一个样, 关键还是在于其中的帧是个什么样子的. 首先帧必须能够接受玩家的输入, 以推进游戏状态; 其次, 每个帧只能接受特定玩家的输入, 比如司马懿发动反馈时, 其他玩家是不能决定反馈区域的. 那么, 帧的声明可能会像这个样子
class FrameBase:
    def react(self, args):
        pass # response to player's action

    def allowed_players(self):
        return [] # which players are allowed
    其中, react 函数的参数 args 就是在之前提到的, 从浏览器端传来的 JSON 解析后的字典数据.
    另外, 当浏览器, 也就是客户端程序向服务器请求 hint 时, 服务器应该给出当前栈顶帧所对应的 hint. 因此, 每个帧都必须还能获得 hint 数据
class FrameBase:
    # other functions

    def hint(self, token):
        if token in map(lambda player: player.token, self.allowed_players()):
            return {} # detail info
        return {} # just who are active players

    虽然说栈大抵都一样, 不过一些必须的功能还是得手动引进, 很关键的就是帧退栈时的动作, 以及如何使帧和帧之间可以传递信息. 下面是结算栈的架子
class ActionStack:
    def __init__(self):
        self.frames = []

    def call(self, args):
        return self.frames[-1].react(args)

    def allowed_players(self):
        return self.frames[-1].allowed_players()

    def hint(self, token):
        return self.frames[-1].hint(token)

    def push(self, frame):
        self.frames.append(frame)

    def pop(self):
        stack_top = self.frames.pop()
        # pass something from stack_top to current stack top
    实现这个 pop 剩余的部分, 需要理清下面的问题
  • 怎么获得之前栈顶的结果
  • 结果怎么被传递给当前栈顶
  • pop 本身由谁在什么时机来调用

Permanent Link: /p/484/

Post tags:

Boarder game

Game development

Python

Sanguosha

基于 B/S 的桌游设计 - 协议

前言

    虽然我觉得国内桌游的始祖可能国粹麻将什么的, 不过貌似这个词在三国杀风靡之后才被正式使用. 这系列文章中, 我也打算使用三国杀来做样例项目, 分析 B/S 结构的桌游设计方式.
    按惯例, 首节内容应该是纯吹水而无码的. 这里就先拔一拔目前比较靠谱的三国杀实现.
    其一是我很敬佩的一个民间 MOD 版本太阳神三国杀, 武将比官网的全, 还有自制武将, 卡牌, 以及各种趣味度爆表的模式. 不过太阳神三国杀是 C/S 一体的, 与我想要设计的并不很搭边.
    另一就是官方的三国杀, 使用 Adobe Flash 制成 (桌面版似乎用的 Adobe Air), 所以也并不能严格算是 B/S 结构. 并且官方版本也不是开源或者开放 API 的, 很难一窥其究竟, 没什么好借鉴的.
    移动终端的三国杀就没怎么实测过了, 我本身也不是智能或智障手机爱好者, 对这方面也不是太有兴趣.
    读者最好知道三国杀的基本规则, 这样一些具体的例子理解起来会更容易.

B/S 架构的游戏

    继续吹一下水. 对于一般的网络应用, 比如 Google Doc 那样的字处理应用, 大部分的逻辑都在浏览器脚本中, 而即使用户去改了浏览器脚本, 导致保存的文档格式有误, 最终受害者也是用户自身. 而多人连线游戏服务器最起码的一点就是要防止用户作弊, 以免破坏游戏状态影响其他玩家, 因此至少服务器端有一份游戏逻辑是必需的.
    现在的问题是, 客户端 (也就是 Browser 端) 是否也复制一分游戏逻辑呢?
    当然可以, 不过假如要重新开发除了浏览器之外, 能够以相同协议与服务器通信的客户端, 或者开放 API, 使得第三方可以自行开发客户端, 那样必然陷入不断重复开发客户端游戏逻辑的无底洞里.
    因此, 除了尽可能将全部游戏逻辑堆在服务器端, 服务器端还应该尽可能指导客户端, 例如告知当前游戏状态, 以及客户端该如何响应.

服务器职责

    好, 吹水结束, 现在理一下, 服务器端应该
  • 完善游戏逻辑, 这是必须的
  • 游戏开始后, 维护游戏状态
  • 接受客户端输入, 返回游戏状态 a
  • 接受客户端输入, 返回谁现在应该如何去做什么事情, 这也就是上一部分最后提到的对客户端的指示
  • 接受指定客户端输入, 推进游戏进程
注 a: 向客户端提供状态有方式有直接提供当前游戏的完整状态, 或提供从游戏某个时机开始的一系列状态增量. 前一种方式每次传输的信息量多, 负担较大, 所以这里采用后者.

协议格式

    B/S 模式下传递信息可以考虑传递 JSON. 而 Python 跟 JS 一样, 是不需要指定变量类型的, 如果发现转出来的值有任何问题就直接认为客户端发送了错误数据就行了, 所以服务器端用 Python 可以省不少事.
    通信内容的具体格式可设计为一个字典, 例如向服务端发送如下信息
{
    'token': '10178a67c867b82392f41c9723cf2daae8fd6ab0',
    'previous event id': 4,
}
    此时服务器返回的可能是如下格式的列表

Permanent Link: /p/483/

Post tags:

Boarder game

Game development

Python

Sanguosha

GAE 速成简易博客 - 简化 RequestHandler

上节回顾 - 进阶数据库操作

在 index.py 和 single_post.py 中, 请求处理器 IndexSinglePost 的代码重复的部分还是挺多的
class SinglePost(webapp.RequestHandler):
    def get(self):
        path = os.path.join(os.path.dirname(__file__),
                            'templates/single_post.html')
        posts = db.GqlQuery('SELECT * FROM Post WHERE pid = :1',
                            int(self.request.get('id')))
        self.response.out.write(template.render(path, {
                'post': posts[0],
            }))

class Index(webapp.RequestHandler):
    def get(self):
        path = os.path.join(os.path.dirname(__file__), 'templates/index.html')
        posts = db.GqlQuery('SELECT * FROM Post ORDER BY date DESC')
        self.response.out.write(template.render(path, {
                'posts': posts,
            }))
作为程序员, 应该对这样的重复代码零容忍, 当机立断, 大刀阔斧来改起!

    从上面的对比看来, 很明显, 对于日常情况中的请求, 服务器端重要的的响应参数包括
  • 模板文件的相对路径
  • 交给模板的参数
    那么很好, 来弄个基类, 新建个 base.py 放进去
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
import os

class BaseHandler(webapp.RequestHandler):
    def put_page(self, templ_file, templ_args):
        path = os.path.join(os.path.dirname(__file__), templ_file)
        self.response.out.write(template.render(path, templ_args))
    开始用新的 BaseHandler 搞起吧.
    先来搞 index.py

Permanent Link: /p/480/

Post tags:

Google AppEngine

Python

Tutorial

Web Server

GAE 速成简易博客 - 更多数据库操作

上节回顾 - 表单处理与基本的数据库操作

现在首页能显示文章列表了, 但是
  • 文章的顺序貌似是乱的, 而一般来说, 博客系统会按照发布的时间先后顺序来放置
  • 构造一个页面, 看指定的某一篇文章的内容
那么, 现在就开始修改数据库吧.

为文章加上 ID 和日期

添加属性

修改 model.py
class Post(db.Model):
    pid = db.IntegerProperty()
    title = db.StringProperty(multiline=False)
    content = db.TextProperty()
    date = db.DateTimeProperty(auto_now_add=True)

def put_post(title, content):
    其中 pid 表示 post id, 是一篇文章的唯一标识; date 是文章的发布时间, 它被设置为对象被存入数据库时自动设置为当前时间 (auto_now_add=True).
    在 GAE 存储中, 并没有类似 auto_increment 的设置, 因此 pid 的管理需手动进行. 在数据库中, GAE 也有给每个对象设置一个全局唯一的 id, 可以通过如 post.key().id() 来获取, 但是这样获取的 id 值在发布服务器上没有规律可言, 不具备有序性, 不建议使用.

按 ID 排序查询和自增 ID

刚刚为 Post 添加的两个属性中, date 是会自动添加到数据库中的, 但 pid 并不会, 得手动给加上. 想要实现自增 ID 的功能, 一个简单的思路是, 从数据库中取出 pid 最大的那篇, 在它的基础上 +1 赋值给新文章即可. 那么继续修改 model.py
def next_post_id():
    posts = db.GqlQuery('SELECT * FROM Post ORDER BY pid DESC')
    return 0 if posts.count() == 0 else posts[0].pid + 1

def put_post(title, content):
    post = Post()
    post.pid = next_post_id()
    post.title = title
    post.content = content
    post.put()
这里 GQL 中的 ORDER BY pid DESC 表示按照 pid 排序, 而且是降序排列.

另外, 还得修改 add_post.py 里的 AddPostHandler 不能让它乱来了, 而应该改为调用 put_post
class AddPostHandler(webapp.RequestHandler):
    def post(self):
      # new_post = model.Post()
      # new_post.title = self.request.get('title')
      # new_post.content = self.request.get('content')
      # new_post.put()
        model.put_post(self.request.get('title'), self.request.get('content'))
        self.redirect('/add_post')

Permanent Link: /p/478/

Post tags:

Google AppEngine

Python

Tutorial

Web Server

GAE 速成简易博客 - 表单处理与数据存取

上节回顾 - 站点基本配置与模板

没有数据滋养的首页还是个半残废, 下面开始折腾数据库.

添加文章

入口

    虽然添加文章这种事情可以直接通过后台暴力搞数据库来做, 但既然要写的是 Web 应用, 那么写个页面提供添加文章的入口也是理所当然的事情.
    页面嘛, 肯定得有个 HTML 模板撑着, 来建个文件, templates/add_post.html


Add Post



Title
Content




    接下来得增加一个 RequestHandler, 新建文件 add_post.py
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
import os

class AddPostEntry(webapp.RequestHandler):
    def get(self):
        path = os.path.join(os.path.dirname(__file__),
                            'templates/add_post.html')
        self.response.out.write(template.render(path, dict()))
    这个文件看起来与 index.py 差不多, 只不过因为 add_post.html 不需要参数, 因此传入 render 的字典是空字典.

    最后在 main.py 中新增一项, 将一个 URL 映射到 AddPostEntry
import wsgiref.handlers
from google.appengine.ext import webapp

import index
import add_post

if __name__ == '__main__':
    application = webapp.WSGIApplication([
        ('/', index.Index),
        ('/add_post', add_post.AddPostEntry),
    ], debug=True)
    wsgiref.handlers.CGIHandler().run(application)
    现在访问 http://localhost:8080/add_post/, 就可以看到这个入口页面了. 不过因为表单还没有设置处理者, 因此点破提交按钮就没有反应的.

处理表单

    还是得添加一个处理请求的类, 修改 add_post.py, 加入这个类
import model

class AddPostHandler(webapp.RequestHandler):
    def post(self):
        new_post = model.Post()
        new_post.title = self.request.get('title')
        new_post.content = self.request.get('content')
        new_post.put()
        self.redirect('/add_post')

Permanent Link: /p/477/

Post tags:

Google AppEngine

Python

Tutorial

Web Server

GAE 速成简易博客 - 开张

前期准备

目前 GAE 官方推荐的 Python 版本是 2.7 (终于脱离了 2.5 的泥潭啊), 实际上 2.6.x 版本也是没问题的 (写个 CMS 这种简易的设备, 应该还用不到太多高端的语言特性).
GAE 当然也是必不可少的. 下载和安装步骤在 Google Code 上都有详细文档. 如果使用 ArchLinux, 还可以通过 AUR 安装. 这里就不废话了.
找个地方, 建立一个目录, 比如叫做 cms-build, 切到这个目录作为工作目录.
下面开始.

数据结构

既然是写博客, 那么最关键的当然要是文章 (post) 了. 先来建立用于定义数据的源文件 model.py
from google.appengine.ext import db

class Post(db.Model):
    title = db.StringProperty(multiline=False)
    content = db.TextProperty()
每篇文章的基本属性有标题 title, 内容 content 和创建时间 date. 其中标题不允许多行; 而内容使用 db.TextProperty 类型而不是 db.StringProperty (根据 GAE 文档, StringProperty 只能存至多 500 字符).

首页

模板

    首先, 将首页给弄出来, 建立目录 templates, 并在这个目录下弄一个 index.html 文件, 内容是


My Blog

{% for post in posts %}
{{ post.title }}
{{ post.content }}
{% endfor %}
    在 GAE 中, HTML 模板文件使用的是 django 的模板语法. 上面文件中, {% for ... %}{% endfor %} 是对传入模板的参数 posts 的迭代. {{ post.title }} 则是对 post 对象的 title 的引用, 而 {{ post.content }} 则是引用 content.
    简而言之 {% xxx %} 是模板中的控制语句, 而 {{ xxx }} 则是引用值. 在 django 官方网站可以看到详细文档, 鄙博客之前也对 django 有过简介.

    这个首页是简单了点, 不过呢, 先看看样子.

视图源文件

    光有 HTML 模板还不行, 至少, Python 源代码才是核心. 现在添加一个 Python 源文件 index.py

Permanent Link: /p/476/

Post tags:

Google AppEngine

Python

Template

Tutorial

Web Server

位运算的优先级

    在现代计算机程序设计语言中, 每一种成熟的语言都支持许多算符. 对于除了像 Lisp 那样表达式本身以中缀式形式书写的语言, 一般语言都会规定表达式的结合顺序, 也就是算符优先级. 由于语言算符数量的庞大, 算符优先级很难整个记下来, 但是通常有迹可循, 比如看到下面的代码
x = y + z
一般会自然地认为 + 将在 = 之前执行, 这是因为很多语言的编译器设计中, 赋值类运算在算术类运算之后进行, 可以认为这是一个常识, 跟乘除在加减之前进行一样自然. 同理下面的代码
x == y and y == z
x == y + z
很自然地能想到, 比较运算 == 会在逻辑运算 and 之前, 而算术运算 + 应该在比较运算之前.

    如果要给算符分个类. 以整数运算为例, 日常二元算符分类的大类应该按照运算结果的类型来分类, 也就是
结果类型算符
整数+ - * / % ^ & | >> <<
boolean== != < > >= <= or and
    从结果类型上也可以看出一些端倪, 比如
x == y + z
这个算式, 如果先计算 ==, 此时 + 左边是 boolean, 而右边是个整数, 这样根本没法圆场了. 所以一般来说, 算术运算拥有较高的优先级完全是天经地义的.

    现在的问题是, 位运算 (^ & |) 到底算啥?
    从之前的说法来看, 它们也应该是算术运算的一种. 不过优先级还真不好说. 最近在某个项目中我将一个后台功能转移到前台, 需要把一段 Python 代码转换成 Javascript 代码, 这时发生了一个悲剧: Python 对于位运算优先级的实现与我的个人观点非常接近, 认为时至今日, 位运算是日常的算术运算之一, 因此它们的优先级要高于比较运算, 也就是说
x + y == x ^ y
在 Python 中等价于
(x + y) == (x ^ y)
而与之相对的, Javascript 认为位运算的优先级要低, 甚至低过了比较运算, 因此上述代码在 V8 引擎眼中等价于
((x + y) == x) ^ y
    这种槽点爆表的设计真是坑死爹了. 好吧, 我去溯源一下, 谁家的语言设计这么疼. 结果原来 C 语言老大哥是这么个整法的, 什么 Javascript 还有个跟这名字差不多不过没有 script 的语言这么承袭的. 哎, 你们这么折腾到底图个啥啊.

Permanent Link: /p/454/

Post tags:

C

Javascript

Operator Precedence

Python

Page 1 2


. Back to Tobary book
Tobary book - Copyright (C) ZhePlus @ Bit Focus
About