PSJay Blog

#FIXME, seriously

Code Review 指南

| Comments

一开始这篇指南是为团队而写的,后来发现包含的内容比较有一般性,所以删掉了一些和团队强相关的内容,发到这里。


「Code Review」直译过来便是「代码复审」,也就是让代码作者之外的人来复审代码,提出修改意见,接受代码变更。

益处

  • 发现错误:一个人的思考总是会不可避免地出现一些纰漏,而这些纰漏在另一个人眼中也许显而易见。
  • 保持代码风格统一:对于整个团队来说,代码风格的统一显得至关重要。风格一致的代码能提供更好的可读性,也能避免犯一些「低级错误」。
  • 保证设计的合理性:代码直接反映出你的系统设计,在写一个新的系统时,确保你的设计不会出现大问题。
  • 保证提交的变更符合团队的其他要求:例如对测试用例的要求和对文档的要求;
  • 互相学习:Code Review 的过程也是思想交流的过程,可以取人之长,补己之短。

怎样进行 Code Review

Review 时机

基本的语法检查和自动化测试通过之后,代码被合并到主分支之前。

Reviewer

Reviewer 可以是一个,也可以更多。在代码变更越大、变更内容牵扯到更多的人和外部系统,或者自信度越低的情况下,你就可能需要更多的 Reviewer。另外在不同的场景下,你也需要不同角色的 Reviewer 参与。

  • 新人上手:Mentor 是第一选择;
  • 日常工作:同组的其他工程师;
  • 向其他系统或模块提交 Patch 或 Bugfix:相应系统或模块的 Maintainer;
  • 专业度很高的代码:团队中相应的领域专家。

Logging,gevent 与死锁

| Comments

前一段时间在工作中遇到了这样一个问题:升级完的一个离线计算任务后,它跑着跑着就会 hang 住,然后服务器的 load 骤降,用 strace 查看系统调用,最后一条指令类似于:FUTEX(0x7ffff79b3e00, FUTEX_WAKE_PRIVATE,...。显然,死锁发生了。

而这次升级,最主要的改动就是将一段比较耗时的 IO 逻辑通过 gevent 和它提供的 Monkey Patch 方法并行化了。代码回滚之后,问题就不存在了。但是奇怪的地方在于,同样的一段并行化的代码,如果是跑在在线请求的环境下,完全不会出现死锁现象,这让我怀疑问题的原因可能并不在于那段代码本身。

于是第二天就和 reAsOn2010 继续调查原因。几经周折后发现,程序在执行最后的 FUTEX 指令之前,一定会执行一条 WRITE 指令,而 WRITE 的内容只是一条简单的日志。

然后,我们发现,程序最终是卡在了 Python 内置 logging 模块的 Handlerhandle 方法中的一行:

1
2
3
4
5
6
7
8
9
def handle(self, record):
    rv = self.filter(record)
    if rv:
        self.acquire() # 卡在了这里!
        try:
            self.emit(record)
        finally:
            self.release()
    return rv

这时,我们才想到,我们的离线任务框架在运行任务之前,会配置 logging 的行为,是不是和这个有关系呢?于是我们把框架中配置 logging 的相关代码注释掉,重启程序,果然,一切正常,没有死锁。

离线任务框架给 root logger 配置了一个 Handler,这个 Handler 会用某种策略将日志通过 socket 发送出去。

这个时候,我们意识到,这段逻辑是在 Monkey Patch 之前运行的!离线任务框架会在做完这些逻辑之后,载入我们编写的 package 并且运行。而 Monkey Patch 的位置,却是在 mypackage/__init__.py 中。

于是我们 hack 了离线任务框架,让 Monkey Patch 在框架的起始位置运行。果然,问题解决,没有死锁。

于是,问题就可以简化成以下模型:

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
32
33
34
35
import sys
import logging
import time

import gevent


class HinderHandler(logging.StreamHandler):

    def emit(self, record):
        r = super(HinderHandler, self).emit(record)
        time.sleep(0.001)  # sleep 1 ms.
        return r


def log_msg(msg):
    logging.info(msg)


def main():
    # Framework configure the root logger
    handler = HinderHandler(stream=sys.stdout)
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    logger.addHandler(handler)

    # Framework start to load our application package
    from gevent import monkey; monkey.patch_all()

    jobs = [gevent.spawn(log_msg, i) for i in range(20)]
    gevent.joinall(jobs)


if __name__ == '__main__':
    main()

HinderHandler 模拟了那个向 socket 发送日志的 Handler,在 Monkey Patch 之前,程序就配置了这个 Handler。

毫无疑问,上面这段代码一旦运行,就会死锁。把 Monkey Patch 那一行移到文件的最上部分,问题就能得到解决。

我们来探究一下到底是怎么回事。

用 `accio` 代替 `import`:深入理解自举和 Python 语法

| Comments

本文翻译自 Replacing import with accio: A Dive into Bootstrapping and Python’s Grammar


Hacker School,我用哈利波特里的咒语覆写内置函数和语句创造了另一个次元的 Python,在 Hacker School 你就能这么干!

尽管这个项目一开始是一个玩笑,但我在著名的 Hacker School 推进者 Allison Kaptur 的指导下,还是快速地深入了解了 Python 的内部实现并且编译了一个 Python 来编译这个 Python。最终都是为了把 import 语句用 accio 代替。

但在我们正式编译哈利波特版的 Python(我爱称它为 Nagini)之前,我们现在说说 Python 内部的一些基础知识,当然,我们以拼写为例。

覆写内置函数(Builtin Functions)

Python 的内置函数存在于 __builtins__ 模块,它在启动的时候会被自动导入。

1
2
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin', 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'cmp', 'coerce', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'intern', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'long', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'raw_input', 'reduce', 'reload', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode', 'vars', 'xrange', 'zip']

覆写 Python 内置函数也是想当地简单:

1
2
3
4
5
6
7
8
9
10
11
>>> wingardium_leviosa = __builtins__.float

>>> del __builtins__.float

>>> float(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'float' is not defined

>>> wingardium_leviosa(3)
3.0

然而,覆写 import 没有那么容易。让我们试试:

1
2
3
4
5
>>> accio = import
  File "<stdin>", line 1
    accio = import
                 ^
SyntaxError: invalid syntax

Python 期待 import 之后会跟上一个模块名,因此它抛出了一个 SyntaxError。这是由于 import x 是一条语句,而不是一个函数调用。

嗯。我想起了我们刚刚在执行 dir(__builtins__) 时被列出的 __import__ 函数。也许我们可以覆写它:

1
2
3
4
5
6
7
8
>>> accio = __builtins__.__import__
>>> accio sys
  File "<stdin>", line 1
    accio sys
            ^
SyntaxError: invalid syntax

# :(

我们把 accio 当作一个函数来调用会发生什么呢?

1
2
3
4
>>> accio(sys)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'sys' is not defined

也许我们应该把 ‘sys’ 当作一个字符串传进去?

1
2
3
4
5
6
7
8
>>> accio('sys')
<module 'sys' (built-in)>

# Ooh!

>>> sys = accio('sys')
>>> sys
<module 'sys' (built-in)>

啊哈。所以 import x 语句也许做了这样的事情:

  1. x 调用了 __import__ 函数: __builtins__.__import__('x')
  2. __import__ 的返回值赋值给模块中的 x

import sys 就像是这个命令的缩写:

1
>>> sys = __builtins__.__import__('sys')

(这里我只描述了简单的 import 语句,但是像 from x import y.w, y.z 的更复杂一些的语句的工作方式也是类似的。)

所以我们有一种方法可以把 accio 作为函数添加,而不是语句。我觉得不快乐。

就找点儿乐子,我们可以把 import 删了么?

1
2
3
4
5
6
7
8
9
10
11
>>> del import
  File "<stdin>", line 1
    del import
             ^
SyntaxError: invalid syntax

>>> del __builtins__.__import__
>>> import os
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: __import__ not found

差不多像那么回事儿!虽然我想要 import os 抛出 SyntaxError 而不是 ImportError,因为很显然 import 是一种输入错误,用户需要知道应该输入 accio

因此,为了完整地把 import 使用 accio 代替,我们将要学习 Python 是怎样定义语句的。

Git 撤销合并

| Comments

Git 的 revert 命令可以用来撤销提交(commit),对于常规的提交来说,revert 命令十分直观易用,相当于做一次被 revert 的提交的「反操作」并形成一个新的 commit,但是当你需要撤销一个合并(merge)的时候,事情就变得稍微复杂了一些。

Merge Commit

在描述 merge commit 之前,先来简短地描述一下常规的 commit。每当你做了一批操作(增加、修改、或删除)之后,你执行 git commit 便会得到一个常规的 Commit。执行 git show <commit> 将会输出详细的增删情况。

Merge commit 则不是这样。每当你使用 git merge 合并两个分支,你将会得到一个新的 merge commit。执行 git show <commit> 之后,会有类似的输出:

1
2
3
commit 19b7d40d2ebefb4236a8ab630f89e4afca6e9dbe
Merge: b0ef24a cca45f9
......

其中,Merge 这一行代表的是这个合并 parents它可以用来表明 merge 操作的线索

举个例子,通常,我们的稳定代码都在 master 分支,而开发过程使用 dev 分支,当开发完成后,再把 dev 分支 merge 进 master 分支:

1
2
3
a -> b -> c -> f -- g -> h (master)
           \      /
            d -> e  (dev)

上图中,g 是 merge commit,其他的都是常规 commit。g 的两个 parent 分别是 fe

Revert a Merge Commit

当你使用 git revert 撤销一个 merge commit 时,如果除了 commit 号而不加任何其他参数,git 将会提示错误:

1
2
3
$ git revert g
error: Commit g is a merge but no -m option was given.
fatal: revert failed

在你合并两个分支并试图撤销时,Git 并不知道你到底需要保留哪一个分支上所做的修改。从 Git 的角度来看,master 分支和 dev 在地位上是完全平等的,只是在 workflow 中,master 被人为约定成了「主分支」。

控制流的抽象

| Comments

本文翻译自 Abstracting Control Flow


所有的程序员都在持续不断地创造抽象,尽管有时候连他们自己也意识不到。我们平常最常抽象的是运算(写成函数)或者行为(子程序或者类),但其实我们的工作中还有一些其他的重复的模式,特别是异常处理、资源管理与优化。

这些重复的模式通常会引入一些规则,例如「关闭所有你打开的东西」,「释放资源然后抛出异常」,「如果成功了则继续,否则……」,这些代码通常都是重复的 if ... else 或者 try ... catch。不如把这些控制流也抽象出来?

在那些没人秀技巧的常规代码里,通常使用控制结构来控制流程。但有时候它们并不能完成得太好,于是我们就只能靠自己动手了。这在 Lisp、Ruby 或者 Perl 里面很容易做到,在所有支持高阶函数的语言中也有办法可以做到。

抽象

让我们从头开始吧。创建一个新的抽象我们需要做些什么呢?

  1. 选择一个功能或者行为。
  2. 给它命名。
  3. 实现它。
  4. 把我们的实现细节隐藏在这个命名之后。

第三点和第四点不一定总是可以做到的。这与你试图抽象的东西和你所使用的语言的灵活性有非常大的关系。

如果你所使用的语言做不到这些,那就忽略实现这一步,仅仅描述实现的方法就好了,然后想办法让它流行起来,从而创造一个新的设计模式。这样你就不会对你将要写的那些重复代码感到糟糕了。

Python 异常捕获的动态性

| Comments

本文翻译自 The Dynamics of Catching Exceptions in Python


我要讨论的异常捕获的动态性,它让我感到吃惊而且可能会隐藏一些 Bug,或者是让我觉得很有趣。

有问题的代码

下面的代码——从产品中稍微(!)抽象出来的代码——看起来很完美。它调用了一个函数来获取一些统计数据然后再以某种方式处理这些数据。最初的数据是从一个套接字(Socket)连接中获取的,这可能会因为一个 Socket Error 而失败。不过由于这些统计数据不是系统中至关重要的一部分,我们仅仅只是用日志记录这个错误然后继续运行。

(注意,我使用 doctest 来检查这篇文章——这就意味着这里的脚本都是真正的代码!)

1
2
3
4
5
6
7
8
9
10
11
12
>>> def get_stats():
...     pass
...
>>> def do_something_with_stats(stats):
...     pass
...
>>> try:
...     stats = get_stats()
... except socket.error:
...     logging.warning("Can't get statistics")
... else:
...     do_something_with_stats(stats)

发现问题

我们的测试工具并没有发有什么地方不对,但实际上只需要注意一下我们的静态分析报告就可以看到问题:

$ flake8 filename.py
filename.py:351:1: F821 undefined name 'socket'
filename.py:352:1: F821 undefined name 'logging'

这段代码的问题就在于我们没有导入(import)socketlogging 模块(module)——并且显然我们没有测试这种情况。让我感到吃惊的是这段代码竟然没有引起一个 NameError ——我觉得异常子句(exception clauses)应该有一些积极的名字搜寻工作(eager name lookup)——毕竟它是用来捕获这些异常的,那么它当然需要知道这些异常的具体类型!

看来不是这样—— except 子句的搜寻行为是非常懒惰的,只有在异常真正抛出的时候它才会被解析。不仅名字搜寻是懒惰的,except 的「参数(arguments)」也可以是任意的表达式。

这也许是好事也许是坏事,又或者是糟糕事。

Python Metaclass

| Comments

本文翻译自 StackOverflow 上的这个答案。


在回答了 yield 关键字和 decorator的问题之后,我更明白了,我决定非常详细地回答这个问题。

读前警告:这个回答非常的长。

类也是对象

在弄明白 metaclass 之前,你应该先清楚地知道什么是 Python 中的类(Class)。Python 中的这种从 Smalltalk 语言中借鉴而来的类十分奇怪。

在大部分的编程语言中,类就是一段用来描述怎样产生对象(Object)的代码。Python 也不例外:

1
2
3
4
5
6
7
>>> class ObjectCreator(object):
...       pass
...

>>> my_object = ObjectCreator()
>>> print my_object
<__main__.ObjectCreator object at 0x8974f2c>

但是 Python 中的类可不止是这些。类本身也是对象。

没错,就是对象。

你一使用 class 关键字,Python 就会执行它并创建一个对象。代码:

1
2
3
>>> class ObjectCreator(object):
...       pass
...

会在内存中创建一个名为 ObjectCreator 的对象。

这个对象(也就是这个类)本身拥有能力来创建对象(它的实例),这就是它之所以是类的原因。

但仍然,它还是一个对象,因此:

  • 你可以将它赋值给一个变量
  • 你可以复制(copy)它
  • 你可以对它增加属性
  • 你可以把它当做函数的参数

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> print ObjectCreator # 你可以打印一个 class,因为它是一个对象
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print o
...
>>> echo(ObjectCreator) # 你可以把类当做函数参数传递
<class '__main__.ObjectCreator'>
>>> print hasattr(ObjectCreator, 'new_attribute')
False
>>> ObjectCreator.new_attribute = 'foo' # 你可以对一个类增加属性
>>> print hasattr(ObjectCreator, 'new_attribute')
True
>>> print ObjectCreator.new_attribute
foo
>>> ObjectCreatorMirror = ObjectCreator # 你可以把一个类赋值给一个变量
>>> print ObjectCreatorMirror.new_attribute
foo
>>> print ObjectCreatorMirror()
<__main__.ObjectCreator object at 0x8997b4c>

动态地创建类

既然类就是对象,那么你就可以像创建对象一样,动态地创建类。

首先,你可以在一个函数(function)中使用 class 创建类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # 返回类,而不是实例
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print MyClass # 这个函数返回的是类,不是实例
<class '__main__.Foo'>
>>> print MyClass() # 你可以用这个类来创建对象
<__main__.Foo object at 0x89c6d4c>

但这还不够动态,因为你还是写了定义类的全部代码。

既然类就是对象,那它一定是由什么东西生成的。

当你使用 class 关键字时,Python 会自动地创建一个对象。但就像 Python 中的大部分事情一样,你也可以手动地完成它。

还记得那个叫 type 的函数吗?这个经典地让你知道一个对象是什么类型的函数:

1
2
3
4
5
6
7
8
>>> print type(1)
<type 'int'>
>>> print type("1")
<type 'str'>
>>> print type(ObjectCreator)
<type 'type'>
>>> print type(ObjectCreator())
<class '__main__.ObjectCreator'>

好吧,type 其实有着非常不一般的能力,它可以动态地创建类。type 可以接受那些描述类的参数,然后返回一个类。

(我懂的,根据传递的参数来决定两种完全不同的作用的函数是很二的,但这是 Python 为了向后兼容而不得不做的权衡)

type 是这样用的:

1
2
3
type(name of the class,
     tuple of the parent class (for inheritance, can be empty),
     dictionary containing attributes names and values)

例如:

1
2
>>> class MyShinyClass(object):
...       pass

可以手动地写成这样:

1
2
3
4
5
>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print MyShinyClass
<class '__main__.MyShinyClass'>
>>> print MyShinyClass() # create an instance with the class
<__main__.MyShinyClass object at 0x8997cec>

你可能注意到了,我们使用“MyShinyClass”作为了类名并且用它作为了一个变量名来引用这个类。它们是可以不同的,但是没有理由把事情搞得更复杂。

type 接受一个字典(dictionary)来定义类的属性。所以:

1
2
>>> class Foo(object):
...       bar = True

可以被翻译成:

1
>>> Foo = type('Foo', (), {'bar':True})

和使用一般的类没什么两样:

1
2
3
4
5
6
7
8
9
>>> print Foo
<class '__main__.Foo'>
>>> print Foo.bar
True
>>> f = Foo()
>>> print f
<__main__.Foo object at 0x8a9b84c>
>>> print f.bar
True

当然,你还可以继承它:

1
2
>>>   class FooChild(Foo):
...         pass

可以翻译成:

1
2
3
4
5
>>> FooChild = type('FooChild', (Foo,), {})
>>> print FooChild
<class '__main__.FooChild'>
>>> print FooChild.bar # bar is inherited from Foo
True

最后,你还可以给你的类定义方法。只需要正确地创建函数并且将它赋值成类的参数就可以了。

1
2
3
4
5
6
7
8
9
10
>>> def echo_bar(self):
...       print self.bar
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

至此你应该明白了:在 Python 中,类本身也是对象,你可以动态地创建它。

这就是你在使用 class 关键字时 Python 所做的事情,你也可以用 metaclass 来完成。

Tornado 模板中变量与参数的命名冲突

| Comments

重现

这个问题是一个多月前遇到的,当时正在为知乎的新版话题编写模板。

我在 Handler 中编写了类似于这样的代码:

1
2
3
def get(self):
    # prepare data
    self.render("new_topic.html", topic = topic, topics = topics, ...)

然后,在模板中写了这样的代码:

1
2
3
4
5
{{topic.name}} <!-- exception raised: UnboundLocalError: local variable 'topic' referenced before assignment -->
<!-- a lot of codes omited -->
{% for topic in topics %}
    <!-- omited -->
{% end %}

正如代码中的注释那样,在注释所在的那一行,会抛出一个 UnboundLocalError 错误,说是 topic 在「声明」之前就被引用了。

当时被这个问题折腾了半个下午,由于模板文件太大,压根就没有注意到是命名引发的冲突,还以为是遇到了什么诡异的 bug。后来大家一起找出了问题所在,一直觉得可以借这个问题看看 Tornado 的模板实现方法,今天刚好有闲情刨根问底。

命名空间(Namespace)

和其他编程语言一样,命名空间的主要作用是用来避免命名冲突。因此,不同命名空间里,可以存在相同的命名。

Python 里存在这样几类命名空间:

  • 模块(module)的全局(global)命名空间;
  • 函数的本地(local)命名空间,它在函数被调用的时候创建,返回或者抛出异常之后被删除。
  • built-in 命名空间,它在 Python 解释器开始运行的时候被创建,并且不能被删除(本质上这是一个叫做 __builtin__ 的模块);
  • 顶级语句(直接从脚本文件或者交互控制台读入的语句)所在的命名空间(本质上这是一个叫做 __main__ 的模块)。

Python 会按照这样的顺序来寻找变量:

  1. 本地命名空间;
  2. 能找到相应变量的最近的外部函数,这里面的变量既不是本地变量,也不是全局变量;
  3. 当前模块的全局命名空间;
  4. built-in 命名空间。

官方的这篇教程中提到:

If a name is declared global, then all references and assignments go directly to the middle scope containing the module’s global names. Otherwise, all variables found outside of the innermost scope are read-only (an attempt to write to such a variable will simply create a new local variable in the innermost scope, leaving the identically named outer variable unchanged).

因此可以知道,在最内层作用域(通常是某个函数内部)访问外部的变量时,这个变量是只读的,如果你尝试去写这个变量,那么 Python 解释器会为你创建一个同名的本地变量,这样做是为了保护那个外部变量的值不会被某个函数轻易改变。(如果你确实需要在函数内部改变某个全局变量,你可以使用 global 关键字。)

因此,毫无疑问地,这段代码会出错:

1
2
3
4
5
6
7
a = 1

def func():
    print a
    a = 2

func()

一个 UnboundLocalError 将会被抛出,因为 func 内部试图对 a 进行写操作,从而 Python 解释器创建了一个名为 a 的本地变量。根据上文提到的变量寻找规则,在 func 内部试图打印 a 时,找到的是本地变量 a,而在这个时候,它还没有和任何对象绑定,因此抛出了这个错误。

Tornado 模板是怎样做的

为了找出问题所在,我阅读了 Tornado 源代码的相关部分。

Tornado 会把模板文件编译成原生的 Python 脚本,然后在渲染视图的时候执行它。关键代码是 tornado.template.Template 类中 generate(self, **kwargs) 方法的这几行:

1
2
3
4
5
namespace.update(kwargs)
exec self.compiled in namespace
execute = namespace["_execute"]
# ...
return execute()

这段代码很容易理解:Tornado 先将 handler 中传递过来的参数保存在一个名为 namespace 的字典中,将其作为命名空间使用,然后把编译好的模板文件(内容就是一个名为 _execute 的函数)在这个命名空间中运行并返回。

根据 Python 官方文档对 exec 语句的描述可以知道:这个名为 namespace 的字典,就是模块运行时刻的「全局命名空间」。因此,我们很容易用 Python 代码来模拟最初的那段错误代码:

1
2
3
4
5
6
7
8
9
10
code = """
def _execute():
    print topic
    topic = 'bar'
"""

namespace = {"topic": "foo"}
exec code in namespace

namespace['_execute']()

Python 解释器为 _execute() 函数创建了 topic 本地变量,而读 topic 的时候,它还没有和任何对象绑定,这就是出错的原因。当然,如果 Python 有块级作用域的话,这个问题就不会存在。

在知乎实习的感触与收获

| Comments

这篇文章是我在知乎“在知乎实习是一种什么样的体验?有何收获?”问题下写的答案


在知乎实习马上就两个月了,自然有很多感触。

在知乎,每个人都能够得到充分的信任。知乎的后端编程语言是以 Python 为主的,而我在来知乎之前,一行 Python 代码也没有写过。让我没想到的是,我用 Java 写的答案通过了知乎的笔试。第二天@申申在电话面试中问我:「你学习过 Python 吗?」我老实回答说:「没有」,心里想着,应该是没戏了。而申申却说:「那你记得来北京之前多学一些 Python。」这种被信任的感觉,让我非常开心。来到知乎之后也一样,作为实习生,直接参与了知乎主站的开发,并且还经常能够得到独自开发一些新 feature 的机会。

在知乎,正能量随处可见,认真、激烈的讨论是家常便饭。我刚来的那几天,坐在我旁边的 @filod@天禄 为了产品上的一个设计激烈地争论了两天,让我误以为 filod 不是前端工程师,而是咱们的产品设计师。工程师在白板前一起讨论问题,解决难题;产品设计师为了一个小细节反复斟酌讨论;运营的同学在一起讨论和思考知乎上发生的每一件事。在知乎就是这样,任何人,在任何时候,任何角落,都有可能发起一个让知乎变得更好的讨论。

在知乎,有爱不止一点点。@奚衡 总是会不厌其烦地回答我提出的种种烦人的问题,向我解释系统的架构,帮我 debug 一个我搞不定的问题,还会和@玉辉 带着我去吃好吃的东西并且还经常不让我付钱 XD。@江泽 也曾经两次主动跑过来拯救了在 bug 面前万分纠结的我。@成城 会时不时的蹦出来,嘴里吐出几个牛逼闪闪的 Vim 插件的名字,让我的 Vim 用起来越来越顺手。@电轻 还会发福利请吃麦旋风!千万别以为这是工程师们自顾自的基情!@马坤姐经常从各种方面照顾着我们,帮我们浇花,帮我们弄到好吃的零食和午餐并且经常还会有神秘美女把新鲜的水果切好然后在工程师的每张桌子上都放上一盘。

在知乎,不止工作时间才在一起!经常在晚饭后和@宏训@昆哥@莫兮哥 打乒乓球;周四晚被@春程 拉去和@张斌 一起打羽毛球,可惜我篮球太挫并且是旱鸭子一枚,不然可以成为知乎全部运动项目参与者(虽然是打酱油……)。

在知乎,让我感到最温暖的事情不是代码更加简洁,程序更加高效,而是早上上班,看到有同学睡眼惺忪地从沙发上起床,工作累了,就在椅子上呼呼大睡的时候。其实啊,大家就像是一家人。就像一家人那样,互帮互助,不求回报;就像一家人那样,茶余饭后,扯淡聊天;就像一家人那样,为着同一个目标奋斗……

说了这么多,只因舍不得。

谢,邀。

Using Markdown !

| Comments

早就想用 Markdown 写东西,只是 WordPress 对 Markdown 的支持有限,所以一直还在坚持用 HTML 写博客。但是文字里面混杂着各种 HTML 标签的感觉实在是太糟糕了,于是从昨天开始计划让博客的编辑平滑地过度到 Markdown 上来。

我先试着安装了几款插件,发现 WordPress 的 Markdown 插件还真是少之又少。上的了台面的似乎就只有两款:

  • WP-Markdown,在保存以及编辑文章的时候对 HTML 和 Markdown 互相转换,并且附带了 PageDown 和一个高亮插件。
  • Markdown on Save,另存一份 Markdown 文本在数据库,在保存文章的时候转成 HTML。

前者很强大,但是在我博客上似乎有些诡异的表现,那就是它会在保存文章之后忽略代码块中的换行,所以代码块中的内容全部挤在了一行。不知道这是个 bug 还是与我的配置不兼容,总之我已经在 GitHub 上提交了一个 issue 给作者。

因此,在问题解决之前,我只能选择后者。Markdown on Save 有一个缺点就是不能向后与 HTML 兼容,也就是说,以前的 HTML 内容,不会自动转换成 Markdown 文本。不过,鉴于我也不怎么经常编辑之前发布的博客,所以这也不是什么大问题。

最头疼的问题就是,为了使博客只存在一个代码高亮插件,我又得转换代码块的标签。之前已经用过几个插件,并且转换过几次,数据库中的格式大概是:

1
<pre lang="lang" line="">code</pre>

现在要转换成:

1
<pre><code>code</code></pre>

于是只好把 wp_posts 表下载下来,写了这个 Python 脚本进行处理:

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
#!/usr/bin/env python
#coding=utf-8

__FILE = 'wp_posts.sql'
__OUT_FILE = 'posts_output.sql'


import re


def __replace(m):
    content = m.group(1)
    return '<pre><code>' + content + '</code></pre>'

if __name__ == '__main__':

    in_file = open(__FILE, 'r')
    out_file = open(__OUT_FILE, 'w')

    pattern = r'<pre\s+.[^>]*?>([\s\S]+?)</pre>'

    for line in in_file:
        out_content = re.sub(pattern, __replace, line)
        out_file.write(out_content)

    print 'done'

覆盖原数据后就差不多大功告成了。BTW, 高亮插件用的是 wp-highlight.js,因为有我最喜欢的 monokai colorscheme,XD。


Update:

WP-Markdown 的作者已经确认并且修复了我上文说的那个 bug,因此,现在我正在使用 WP-Markdown,它工作得非常好。