Python闭包陷阱

闭包是指在函数中定义子函数。比较经典的用法是让子函数访问父函数中的局部变量,或者作为返回值返回并延迟执行。

Python中也可以使用闭包,但子函数中使用的外部变量在执行时才求值。因此,如果外部变量在子函数定义和执行之间发生变化,则会影响到最后的执行结果,本人就在最近犯了这个编程错误。Stackoverflow也讲到这个问题,下面给出一个具体的例子。

def foo():
    funcs = []
    for i in xrange(4):
        def test():
            print i
        funcs.append(test)
    for func in funcs:
        func()

上面的例子里,我们希望用一个循环构建多个子函数并延迟执行,原本以为可以输出0,1,2,3,但实际输出是3,3,3,3,原因是执行时i的值已经变为3。

一种解决方法是,在子函数里使用参数代替外部变量,同时把参数名字绑定到外部变量变化前的值,如下所示。

import functools

def foo():
    funcs = []
    for i in xrange(4):
        def test(j):
            print j
        funcs.append(functools.partial(test, i))
    for func in funcs:
        func()

上面的例子里,我们利用functools模块构建一个新的函数调用原来的子函数,同时把子函数的参数名绑定到当时i绑定的对象。最后,得到预期的输出。

获取线程的返回值

在Python中,创建线程通常利用threading模块,创建Thread对象时提供相应的函数作为参数,当线程开始时,就会自动执行该函数。下面的代码说明了这个过程。

import threading

def foo():
    pass

t = threading.Thread(target=foo)
t.start()
# Some time elapsed
t.join()

调用者最后会阻塞在join()调用里,直到线程结束并返回。这与等待子进程结束的wait(2)系统调用有点类似,但wait(2)系统调用返回子进程退出的状态(正常退出的退出码或者由信号引起退出),而join()调用却没有任何返回。

主要原因是,join()是作为同步功能的调用,不仅父线程可以调用join(),其他线程也可以调用此线程对象的join()方法,join()方法可以调用多次。

问题是我们有时确实需要代表线程逻辑的函数的返回值。既然不能利用join()取得返回值,那么最直接的方法是,线程函数在结束前把结果保存在一个共享变量上,调用者调用join()之后再通过共享变量取得结果。这个方法有几个问题:第一,有时线程运行的函数是第三方编写的,不受管理;第二,这种方法加强了调用者和被调用者(线程函数)之间的耦合,在代码变化时影响较大。

最近在写代码的过程里,发现了一个更好的方法。利用Python中函数是第一类对象的特点,调用者可以基于被调函数构建一个涉及共享变量操作的函数,而线程则执行这个新的函数。下面的代码展示了这种方法。

import threading

return_val = None
def thread_func():
    global return_val
    return_val = foo()
t = threading.Thread(target=thread_func)
t.start()
# Some time elapsed
t.join()
print return_val

这样,调用者就可以在不污染被调用模块的代码的前提下,用背景线程执行一个任务,并获取其返回值。

用线上数据库做开发

我在开发Web网站的时候,一般在本地调试好没问题后,再上传到服务器。本地调试时,数据从哪里来呢?通常我会在刚开始的时候生成一些伪数据作为测试用途,如果是已经上线的系统,则我可能会dump一部分数据(通常一个月)下来,然后测试。然而,每次都这么做则要耗费不少时间,而且还要占据本地的存储空间。另外,如果换台机器开发,这个过程可能还要重复一次。

直到最近一次阅读中,发现ssh有-L这个参数,使得我可以把远程数据库当作本地数据库使用。具体来说,-L参数可以把发往本地端口的数据透明地传送到指定目标的指定端口。-L port:host:hostport,其中port是本地监听的端口,host是目标主机(从ssh服务器发起连接),hostport是目标主机的端口。因此,-L 3306:localhost:3306,就能把发往本地3306端口的数据转发到ssh服务器的3306端口,从而在本地可以直接访问MySQL服务器。

MySQL connection

值得注意的是,如果这时直接连接服务器,例如mysql -u root,那么会提示错误,如上图所示。这是因为,mysql如果发现服务器是localhost时,会自动使用socket file,而不是建立TCP连接。解决方法是让mysql客户端强制使用TCP连接,即加--protocol=TCP;或者指定host为127.0.0.1,即-h 127.0.0.1。

除了文中的例子外,端口转发还能做很多事情,大家不妨发散思维。值得注意的是,直接利用线上服务器做开发有一定风险,只涉及查询数据的开发我才敢这么做,如果涉及到修改数据,那还是用测试数据库吧。

窗口过高的问题

一般情况下,如果某个应用程序的窗口过大,我们可以把鼠标放到窗口的四周,然后调整大小;或者右键单击标题栏,选择“改变大小”;或者直接把窗口最大化。但是,有某些应用程序会有最小高度(或最小长度),无论怎么改变窗口大小都不能比最小高度小,比如Android的模拟器。

Move Window

Gnome下可以通过“移动”窗口解决这个问题。首先,第一步可以通过右键单击标题栏,选择“移动”启用移动窗口,或者直接用快捷键Alt+F7也可以启用;第二步则用鼠标或键盘的方向键移动窗口的位置;最后单击或回车确定。上图中,是我把Android模拟器向上移动后的样子。

其他桌面环境应该也有类似的功能。遗憾的是,Windows XP的窗口移动后,如果超出屏幕范围,它又自动调整窗口的位置和大小,使之前的移动没有效果。

初学netcat

前几天,从网络中得知netcat这一被称为“网络瑞士军刀”的神器后,学习了一些基本的用法,现在总结一下。

使用netcat,我们可以把计算机A上的文件传到计算机B上。根据建立连接方式的不同,我们有两种方法。方法一,计算机B负责监听,A向B主动连接。这时在B上先输入“nc -l 8000 > file”,其中8000是监听的端口,然后在A上输入“nc IP 8000 < file”。方法二,计算机A负责监听,B向A主动连接。这时先在A上输入“nc -l 8080 < file”,然后在B上输入“nc IP 8080 > file”。

netcat从名字上是net+cat,但是它和cat有着本质上的不同。cat把标准输入的内容拷贝到标准输出,是单向的。然而,netcat是双向的,两台计算机的netcat建立连接后(不妨把两台计算机称为A和B),A从标准输入读到的东西,会经过网络,然后从B的标准输出出来,反之亦然。

netcat

我觉得这种I/O很像立交桥,它能同时处理两路I/O,然而,把netcat用于传文件显然只能用到其中一路I/O。如果能同时用上两路I/O,会发生什么有趣的事情呢?首先,如果两端的netcat的输入和输出都是接Terminal,那么两端的人可以用nc聊天,如上图所示。

如果把监听的一端(不妨称为计算机B)的输入输出重定向,那就更有趣。比如把B上nc的输出接到bash,然后把bash的输出又接回nc,那么B就是一个bash服务器,相当于开了个后门。具体实现可以使用命名管道和重定向,

mkfifo named_pipe; bash named_pipe | nc -l 8000 > named_pipe

同理,如果把程序换为cat,那么B就是一个echo服务器;如果把程序换为date,那么B就是一个date服务器。但和真正的服务器不一样,B只能接受一个连接,并且接受一次连接后就马上退出。

制作自签名的证书

当搭建一个内部网站系统时,我们可能会使用自签名(self-signed)的证书,它的主要目的不是防伪,而是使用户和系统之间能够进行SSL通信,保证密码等个人信息传输时的安全。

一般情况下,制作证书要经过几个步骤,如上图所示。首先,用openssl genrsa生成一个私钥;然后,用openssl req生成一个签署请求;最后,把请求交给CA,CA签署后就成为该CA认证的证书了。

如果在第二步请求时加上-x509参数,那么就直接生成一个self-signed的证书,即自己充当CA认证自己。

除了这种方式外,在Debian或者Ubuntu系统中有更加简便的方法制作self-signed证书——使用make-ssl-cert(1)命令。该命令在ssl-cert的包里,一般会伴随着Apache的安装而安装,可能单独安装也可以。

make-ssl-cert generate-default-snakeoil命令生成一对万金油式的公钥密钥,可以直接使用。其中,公钥(证书)在/etc/ssl/certs/ssl-cert-snakeoil.pem,密钥在/etc/ssl/private/ssl-cert-snakeoil.key。另外make-ssl-cert还可以根据模板生成公钥密钥,并保存在其他位置,这样我们可以在默认的模板文件的基础上添加一些信息,如Organization等,默认的模板文件在/usr/share/ssl-cert/ssleay.cnf。例子:

# make-ssl-cert /usr/share/ssl-cert/sslsvn.cnf /home/svn/sslsvn.pem

我利用模板文件生成了一个公钥私钥文件,它们的内容放在了一起,一般问题不大,如果实在需要分开,手工分开即可。还有一点值得注意,我发现Debian里,make-ssl-cert是只能由root执行的命令。

使用这个方法,我们就可以快速方便地生成一个或多个公钥密钥对。

设定Ubuntu启动服务

当我们安装了一些Ubuntu的服务后,有时希望配置它为自动启动或不要自动启动。比如我最近安装了tor和privoxy,我希望设定它们不要自动启动。

怎样做呢?Ubuntu虽然是使用upstart进行启动,但是Ubuntu还是模拟了System V的运行级别体系。首先,服务脚本要出现在/etc/init.d/下面。然后/etc/rc0.d/到/etc/rc6.d/分别代表0到6的运行级别,每个级别对应的目录下面,有着刚才init.d目录里脚本的软链接,但是名字和原来不完全一样,Ubuntu启动的时候就是根据rcX.d/目录里软链的名字做出相应的动作。

比如我的Ubuntu 8.10默认是进入运行级别2的,如果rc2.d/目录下有S20tor软链接,表示启动的时候自动运行tor服务。软链接名字的第一个字符或者是S,或者是K,S表示start,K表示stop。接下来数字NN表示优先级,20是默认值,启动的时候先执行K开头的服务,在其中按NN从小到大执行,接下来执行S开头的服务,也是按NN从小到大执行。比如目录里有K50,K60,S10,S20,那么启动到该运行级别时按K50->K60->S10->S20的顺序执行。优先级的目的是提供服务的依赖性,比如A服务要依赖于B服务的运行,可以设置B为S20,A为S21,然后stop的时候要相反顺序。为什么先执行K呢?那是因为切换运行级别的时候,先关闭服务再开启的顺序不容易乱。

说了这么久,相信读者都明白配置Ubuntu启动服务是否自动运行可以直接编辑软连接的名字了。要自动运行,就是要在相应运行级别下加入S开头的软链接。如果不要自动运行,或者把链接删掉,或者设为K开头的名字。需要指出的是,把软链接删掉是不正确的方法,因为当软件包升级的时候,升级脚本运行update-rc.d命令,就会重新加入那个软链接,因此正确的方法是把S改为K,这样不会受update-rc.d命令影响。进一步要指出的是,我们最好也使用update-rc.d命令去进行软链接的操作。

典型用法一,把服务加到2,3,4,5级别自动运行,加到0,1,6自动停止。

sudo update-rc.d foobar defaults

或者

sudo update-rc.d foobar start 2 3 4 5 . stop 0 1 6 .

典型用法二,把服务设定为启动时自动停止。

sudo update-rc.d -f foobar remove
sudo update-rc.d foobar stop 0 1 2 3 4 5 6 .

值得注意的是,如果任意一个rcX.d/目录已经有某脚本的链接,必须像刚才的命令第一行一样先删除原来的链接,再进行添加。