使用并查集完成的一个需求

需求是这样的:有一个文件,每行有两个字段,帐号和IP。

规则:

  1. 同IP登录的帐号归为同一工作室
  2. 有帐号交叉的工作室归为同一工作室。

输出:

表1:工作室编号 帐号,表2:工作室编号 IP

思考:

从规则1可以想到,可以把一个工作室看作一棵树,根节点是一个IP,所有字节点是这个IP登录过的帐号。那么规则2就可以看成,两个树的某个子节点是交叉的,就把两棵树连成一个森林。

而其实可以看成有两个不相交的IP集合(例如刚开始两个IP下面的帐号并无交叉,此时每个IP集合里面只有1个IP),因为某个条件(帐号交叉了),需要把两个IP集合合并,很容易想到是并查集的应用。

具体的做法是:在遍历文件的过程中,生成字典ipdict,key是IP,value是一个set,元素是帐号。同时生成一个字典first_urs_dict,key是帐号,value是这个帐号第一次出现的IP,为什么要这个字典呢?是因为我们只关心登录了两个或以上IP的帐号,当帐号第一次出现时,就把其IP记下,当帐号第二次出现时,看到first_urs_dict存在key,那么这个帐号就是登录了两个IP了,可以把这个IP并到一个集合里了。

而并查集是提供查询某n个元素是否在同一个集合的算法。它只需要定义两个操作find(u)和union(u, v),使用Python描述就是:

root = {}
def find(u):
    if u not in root:
        root[u] = u  #初始化,每个元素的根是它自己
        return u

    if find(u) == u:
        #根是自己,立即返回
        return u
    else:
        _root = find(u)
        root[u] = _root #路径压缩
        return _root


def union(u, v):
    ru = find(u)
    rv = find(v)

    if ru != rv:
        #两个元素的根不同,把它们的并起来
        #做法就是把其中一个根成为另一个根的子节点
        root[rv] = ru

当find(x)和find(y)的结果是相等的,可以说明x和y在同一个集合里面。

至此,把两个IP-URS树合并的关键一步已经解决。到算法结束,同一个森林里面的IP和URS自然属于同一个工作室了。

附上完整的代码:

#!/usr/bin/env python

import os, sys
import time

root = {}
def find(ip):
    if ip not in root:
        root[ip] = ip
        return ip

    rip = root[ip]
    if rip == ip:
        return ip
    else:
        _root = find(rip)
        root[ip] = _root
        return _root

def union(u, v):
    ru = find(u)
    rv = find(v)
    if ru != rv:
        root[fv] = fu

def main():
    ip_dict = {}
    first_urs_dict = {}
    count = 0
    for line in sys.stdin:
        count += 1

        if count % 100000 == 0:
            print >> sys.stderr, count

        urs, ip = line.rstrip('\r\n').split('\t', 1)
        if ip not in ip_dict:
            s = set()
            ip_dict[ip] = s
        else:
            s = ip_dict[ip]

        if urs in first_urs_dict:

            fip = first_urs_dict[urs]
            if fip != ip:
                union(fip, ip)

        else:
            s.add(urs)
            first_urs_dict[urs] = ip

    total_dict = {}

    fname_ip = 'no_ip.txt'
    fname_urs = 'no_urs.txt'
    fip = open(fname_ip, 'w')
    furs = open(fname_urs, 'w')
    no = 0
    for ip, urs_set in ip_dict.iteritems():
        r = find(ip)

        if r not in total_dict:
            total_dict[r] = no
            _no = no
            no += 1
        else:
            _no = total_dict[r]

        print >> fip, "%s\t%s" % ( _no, ip)

        for urs in urs_set:
            print >> furs, "%s\t%s" % (_no, urs)

    fip.close()
    furs.close()



if __name__ == '__main__':
    main()

《Python高级编程》读书笔记:方法解释顺序浅析

Python在2.2引入了New-style object(ref),而且在2.3引入了新的方法解释顺序(Method resolution order,以下简称MRO),新的MRO解决了多继承下的方法解释顺序问题。

考虑如下代码:

class BaseBase:
	def foo(self):
		print 'BaseBase'

class Base1(BaseBase):
	pass

class Base2(BaseBase):
	def foo(self):
		print 'Base2'

class MyClass(Base1, Base2):
	pass

o = MyClass()
o.foo() // prints 'BaseBase'

因为在Old-style object,方法解释顺序是“深度优先且从左往右”,MyClass解释foo方法时,在Base1(左边这路)找到BaseBase,就返回BaseBase的foo方法了。这跟从最近的继承层次开始找的直觉不同。

对于New-style object,引用了C3解释方法:
定义一个列表的头和尾部。对于列表[C1, C2, C3],其头是C1,尾是[C2, C3]。(熟悉函数式编程的同学应该不陌生)

C3方法公式如下:
L[MyClass(Base1, Base2)] = MyClass + merge(L[Base1], L[Base2], Base1, Base2)
其中L为类的线性化继承层次

Merge算法是:
1. 取第一个列表的头(即L[Base1][0]),如果这个头没有出现在任何列表的尾部,就把它加到L[MyClass]中,并从合并中的列表删除;否则查找下一个列表的头,作同样的判断。
2. 如果所有类都被删除,则算法成功完成;如果不能找到符合条件的列表头(好的表头),则失败,Python拒绝创建MyClass类

通过类的__mro__可以看到类的MRO列表

In [13]: MyClass.__mro__
Out[13]: 
(<class '__main__.MyClass'>,
 <class '__main__.Base1'>,
 <class '__main__.Base2'>,
 <class '__main__.BaseBase'>,
 <type 'object'>)

对于很多更复杂的情形,附录文档有更详尽的解释

参考文档:
The Python 2.3 Method Resolution Order


《Python源码剖析》读书笔记:内存垃圾回收

  • Python内存回收的基石是引用计数,“当一个对象的引用被创建或复制时,对象的引用技术加1;当一个对象的引用被销毁时,对象的引用技术减1”,如果对象的引用计数减少为0,将对象的所占用的内存释放。
  • 引用计数的缺点是对循环引用无能为力,优点是将内存释放的操作时机离散化,不会引起瞬间的大波动。
  • Python采用Mark-Sweep算法来解决循环引用问题
  • Mark-Sweep过程
    ** 寻找root object集合,root object指全局引用和函数栈上的引用
    ** 从root object出发,通过其每一个引用到达的所有对象都标记为reachable(垃圾检测)
    ** 将所有非reachable的对象删除(垃圾回收)

  • 垃圾检测只需要跟踪可以保有其他对象引用的container对象(例如list,dict,class,instance等)即可。

  • Python创建container对象时,会在前面加入PyGC_Head。通过PyGC_Head串起来的双向链表把所有container对象串起来。
  • Python的GC有分代机制,分3代。最年轻代到最老代默认个数是700/10/10,当某代里的对象数超过阈值,将会引发一次GC动作。某个对象在一次GC中存活的话,就把它移到更老的一代。
  • 对象的引用计数存在PyGC_Head的ob_refcnt,GC时会使用此值的副本,PyGC_Head的gc.gc_refs
  • 在垃圾检测过程中,会先试探地把所有对象的引用计数减1。遇到一个引用计数为0的对象,先将其标记为GC_TENTATIVELY_UNREACHABLE。如果之后某个对象的遍历过程中遇到这个临时标记的对象,证明它还是有引用的,将其移回reachable列表并且把引用计数设为1。
  • 垃圾检测完成后,所有在unreachable链表的对象都可以释放了,除了那些定义了__del__方法的对象。之前一篇blog也提到,Python不会自动回收这些对象。

《Python源码剖析》读书笔记:多线程

(如无特别注明,以下笔记都是基于Linux平台)

  • GIL覆盖面不只是Python的解释器,还包括Python的C API
  • Python线程根据”已执行指令数”来切换线程,在Python 2.5+,这个阈值是100
>>> import sys
>>> sys.getcheckinterval()
100

但有些字节码指令执行并不会增加这个计数,因此线程切换时已执行指令数可能会比这个略多
** 当字节码执行之后通过goto转移到fast_next_opcode,这时不会更新计数器
** 线程通过阻塞调度,并不会重置计数器

  • GIL并不是随着解释器启动而创建的,只有用户第一次使用thread的时候创建。
  • 在不同平台上GIL使用不同的实现,在Linux使用信号量。
  • 创建线程使用pthread库,Python线程是使用系统原生线程。
  • 子线程创建后马上申请GIL
  • PyThreadState保存了这个线程的上下文信息
  • 当一个活动线程挂起后,调度到哪个线程由操作系统线程调度决定。当前活动线程挂起前,会释放GIL

  • Python线程调度有两种方式,一种是标准调度,另一种是阻塞调度。
    ** 标准调度是当前线程已执行指令数大于100时主动释放GIL,让其他线程有机会运行。
    ** 阻塞调度是当前线程执行到阻塞的系统调用(例如time.sleep)之前,释放GIL,让其他线程有机会运行。

  • thread模块提供基础的Lock对象,默认使用信号量,如果在信号量有问题的情况下,使用pthread_lock

  • 当前线程对Lock对象加锁前,先释放GIL,获得锁后再获取GIL。(Threadmodule.c:38)
  • threading模块是Python写成的。里面的RLock/Condition/Semaphore/Event锁对象都是基于thread.Lock
  • threading模块的Thread启动分两步,第一步使用thread.start_new_thread创建原生线程,原生线程执行__bootstrap函数,此时线程状态为创建中,放入_limbo字典;第二步在__bootstrap函数中将自己放到_active字典,以线程ID为key,然后执行run函数

PS: 求购正版《Python源码剖析》,或求提供陈儒联系方式,谢谢!


使用pep8 vim插件规范Python代码

Python有官方的代码风格指导——PEP8,但程序员不可能费脑子去记住全部,程序员应该写程序来执行这样的操作。于是有人写了个Python程序来执行PEP8检测。世界上又有一撮人,他们叫Vim党,其中有人写了Vim的插件,来调用这个PEP8检测程序,并使用Vim的Quickfix功能来呈现结果。

安装

1.

sudo pip install pep8

2. 到http://www.vim.org/scripts/script.php?script_id=2914下载最新版本的插件,放到~/.vim/ftplugin/python/目录下
3. 打开vim, 输入:filetype,看看plugin是否ON。如果不是,编辑~/.vimrc,加入filetype plugin on这行
4. 用vim打开一个.py文件,按F5看看是否有Quickfix List出现,或者左下角是否有提示”PEP8 correct”,有两者其一说明安装成功。

不过个人觉得PEP8有点啰嗦,有些规定(例如一行最多79个字符)显得有点过时,仅作为一个参考吧。


注意Python中strptime的效率问题

Python中datetime,time等类型都有strptime方法,将时间字符串根据格式解析成相应的对象。很多时候我们的需求只是解析”%Y-%m-%d %H:%M:%S”格式的字符串,而strptime会根据locale作相应不同的处理,增加了不必要的复杂度,在某些场合成为了性能瓶颈。

在python的mail-list上早有人提出这个问题,里面提到使用正则表达式解析。

遂动手测试下:

#!/usr/bin/env python
import datetime
import os
import re
import timeit, cProfile

def strptime():
    with open('time.txt', 'r') as f:
        for line in f:
            line = line.rstrip(os.linesep)
            dt = datetime.datetime.strptime(line, "%Y-%m-%d %H:%M:%S")


def reg():
    rep = re.compile(r'(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):(\d{2})')
    with open('time.txt', 'r') as f:
        for line in f:
            line = line.rstrip(os.linesep)
            m = rep.match(line)
            dt = datetime.datetime(int(m.group(1)),
             int(m.group(2)),
             int(m.group(3)),
             int(m.group(4)),
             int(m.group(5)),
             int(m.group(6))
             )

if __name__ == '__main__':

    t1 = timeit.Timer("reg()","from __main__ import reg")
    t2 = timeit.Timer("strptime()", "from __main__ import strptime")

    cProfile.run("t1.timeit(3);print")
    print""
    cProfile.run("t2.timeit(3);print")

在开发机上得到如下结果:

         72741 function calls (72698 primitive calls) in 1.119 CPU seconds

         219657 function calls (219510 primitive calls) in 3.176 CPU seconds

可见提升还是很大的。

2011-10-17 EDIT: 有童鞋在评论提出另一种方法绕过语言检查,提高strptime速度:

import _strptime
_strptime._getlang = lambda: (None, None)

测试结果:

         138657 function calls (138510 primitive calls) in 2.161 CPU seconds

比起未优化的有1/3提升,但仍是正则优化的两倍耗时,供大家参考


使用gdb调试Python进程

有时我们会想调试一个正在运行的Python进程,或者一个Python进程的coredump。例如现在遇到一个mod_wsgi的进程僵死了,不接受请求,想看看究竟是运行到哪行Python代码呢?这时就需要祭出gdb这个神器了。

准备

1. 确认你的gdb版本是>=7,gdb从版本7开始支持对Python的debug。 (ref)

2.确认gdb连接的Python是所要debug的Python,否则请重新编译gdb。

方法:

$ gdb
(gdb) python
>import sys
>print sys.version
>end
2.4.3 (#1, Sep 21 2011, 19:55:41) 
[GCC 4.1.2 20080704 (Red Hat 4.1.2-51)]

在一些追求稳定的发行版(例如CentOS),Python的版本会较低,这时都会自己编译一个Python使用。而从源里安装的gdb会连接源里Python的版本。例如在CentOS 5.4,源里的Python是2.4.3,从源安装的gdb也会连接到Python 2.4.3。

编译时注意,要把自己编译的Python路径加到PATH环境变量里,这样gdb configure的时候才会找到新版Python并连接。

3.下载libpython.py

如何Debug

假设要debug的进程号为1000

$ gdb -p 1000

使用此命令即可使gdb附加到进程。

载入libpython脚本

  • 如果你的gdb是redhat或fedora等厂商修改过的,会有--python选项,使用此选项即可指定gdb启动时载入的Python扩展脚本(此脚本是扩展gdb的,不是我们需要debug的脚本)。
    $ gdb --python /path/to/libpython.py -p 1000
    
  • 如果安装的是GNU的gdb,就需要打开gdb后手动载入libpython.py脚本

    (gdb) python
    >import sys
    >sys.path.insert(0, '/path/to/libpython.py')
    >import libpython
    >end
    (gdb)
    
  • 这时就可以使用py-bt命令打印当前线程的Python traceback了

    libpython还提供很多命令,例如py-print打印变量,py-locals打印所有本地变量等等,详细可打开libpython.py查看。

    一点经验

  • 在gdb可以使用generate-core-file命令生成一个coredump文件。之后可以用gdb –core来打开coredump文件进行debug。避免一直attach住进程,可以快速重启恢复服务
  • gdb-heap是gdb的一个扩展。可以打印Python的内存使用情况
  • 参考资料

  • DebuggingWithGdb
  • EasierPythonDebugging
  • Debugging with gdb (gdb documentation)

  • Django下解决IE8使用兼容性视图的问题

    IE8及之后的版本为了兼容针对老版本IE而编写的网页,使用了”兼容性视图设置”,并内置了很多模式(Browser Mode)。IE会根据doctype, meta, http response header, IE设置(ref1)(ref2)等来选择一个模式来渲染。

    新写的页面都应该遵循标准且用html5 doctype,这样大多数情况下IE都会使用标准模式来渲染。但这次写的是内部系统,处于Intranet环境下。很多IE的兼容性视图设置都勾选了”在兼容性视图中显示Intranet站点”。当IE勾选了这个选项后,会忽略页面meta标签的指示。是否有一种可控的方法override掉这个设置呢?答案是肯定的。方式就是在HTTP的响应中加入X-UA-Compatible头。

    一般X-UA-Compatible可以在Web Server例如Apache或Nginx中设置,但有可能一个Web Server不同的应用程序需要不同兼容性视图设置。根据实际情况,我选择在Django里加入这个header。

    已经有人写好Django的Middleware了,按照其说明安装好Middleware,在IE刷新即可看到结果。

    参考资料:


    在Heroku上部署web.py应用

    今天Heroku宣布支持Python,原本一直眼馋Ruby的同学有Heroku这个云主机平台,终于等到支持Python,立刻尝鲜一把。

    安装Heroku环境

    虽然系统是Ubuntu 10.04,但因为不想增加apt-get-repository,我没有使用官方推荐的apt-get方式。采用tarball方式,下载解压运行,提示没有ruby环境,那就安装吧:

    sudo apt-get install ruby rubygems libreadline-ruby libopenssl-ruby
    

    然后手动把解压出来的heroku-client文件夹加到PATH路径里。

    配置Heroku环境

    此处借用Heroku的文档,配置Heroku登录凭据与公私钥

    $ heroku login
    Enter your Heroku credentials.
    Email: adam@example.com
    Password: 
    Could not find an existing public key.
    Would you like to generate one? [Yn] 
    Generating new SSH public key.
    Uploading ssh public key /Users/adam/.ssh/id_rsa.pub
    

    编写hello world

    Heroku文档是介绍使用flask这个web框架,由于习惯使用web.py,所以使用web.py来编写hello world。

    编写pip的requirements.txt

    $ echo "web.py==0.36" > requirements.txt
    

    创建一个virtualenv

    $ virtualenv --no-site-packages .
    New python executable in ./bin/python
    Installing setuptools............done.
    
    $source bin/activate
    

    然后根据requirements.txt安装环境:

    $ bin/pip install -r requirements.txt
    Downloading/unpacking web.py==0.36 (from -r requirements.txt (line 1))
      Downloading web.py-0.36.tar.gz (87Kb): 87Kb downloaded
      Running setup.py egg_info for package web.py
    Installing collected packages: web.py
      Running setup.py install for web.py
    Successfully installed web.py
    Cleaning up...
    

    编写.gitignore,将以下内容写到.gitignore文件里

    bin
    build
    include
    lib
    .Python
    *.pyc
    

    编写web.py程序

    终于开始写干活的程序,直接上代码:

    $ vi hello.py
    
    import web
    import os, sys
    
    urls = (
        '/(.*)', 'hello'
    )
    
    app = web.application(urls, globals())
    class hello:
        def GET(self, name):
            if not name:
                name = 'World'
            return 'Hello, ' + name + '!'
    
    if __name__ == "__main__":
        port = os.environ.get("PORT", "5000")
        sys.argv[1] = port
        app.run()
    

    由于Heroku会提供PORT环境变量来指定应用程序的端口,而web.py默认使用命令行第一个参数作为端口,因此直接获取环境变量PORT,放到sys.argv[1]里,注意web.py从命令行参数读端口参数,端口参数是字符串。

    使用Foreman作为启动脚本

    Heroku是ruby系的产品,使用Foreman作为启动脚本

    使用rubygems即可安装foreman

    $ gem install foreman
    

    在ubuntu 10.04上安装foreman,会遇到错误:

    ERROR:  Error installing foreman:
            thor requires RubyGems version >= 1.3.6
    

    而ubuntu 10.04的默认源里的rubygems是1.3.5的,参考stackoverflow一篇文章,找到解决方法:

    sudo gem install rubygems-update
    cd /var/lib/gems/1.8/bin
    sudo ./update_rubygems
    

    然后编写Procfile,这个Procfile是Foreman所读取的启动项

    echo "web: python hello.py 5000" > Procfile
    

    然后就可以启动来看下

    $ foreman start
    13:20:59 web.1     | started with pid 25656
    13:21:00 web.1     | http://0.0.0.0:5000/
    

    正常。

    部署到Heroku

    Heroku使用git push的部署方式,当代码push到远端,Heroku会自动更新代码并重启应用,使用过git的同学应该不会陌生。

    $ heroku create --stack cedar
    Creating simple-mountain-9034... done, stack is cedar
    http://simple-mountain-9034.herokuapp.com/ | git@heroku.com:simple-mountain-9034.git
    Git remote heroku added
    

    然后推送到远端:

    $ git push heroku master
    Counting objects: 6, done.
    Delta compression using up to 4 threads.
    Compressing objects: 100% (3/3), done.
    Writing objects: 100% (6/6), 592 bytes, done.
    Total 6 (delta 0), reused 0 (delta 0)
    
    -----> Heroku receiving push
    -----> Python app detected
    -----> Preparing virtualenv version 1.6.1
           New python executable in ./bin/python2.7
           Also creating executable in ./bin/python
           Installing setuptools............done.
           Installing pip...............done.
    -----> Installing dependencies using pip version 1.0.1
           Downloading/unpacking web.py==0.36 (from -r requirements.txt (line 1))
           Creating supposed download cache at /app/tmp/repo.git/.cache/pip_downloads
             Storing download in cache at /app/tmp/repo.git/.cache/pip_downloads/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fw%2Fweb.py%2Fweb.py-0.36.tar.gz
             Running setup.py egg_info for package web.py
               
           Installing collected packages: web.py
             Running setup.py install for web.py
               
           Successfully installed web.py
           Cleaning up...
    -----> Discovering process types
           Procfile declares types -> web
    -----> Compiled slug size is 2.6MB
    -----> Launching... done, v3
           http://simple-mountain-9034.herokuapp.com deployed to Heroku
    
    To git@heroku.com:simple-mountain-9034.git
     * [new branch]      master -> master
    

    这里注意要修改web的进程数:

    $ heroku scale web=1
    Scaling web processes... done, now running 1
    

    然后可以查看进程的状态:

    $ heroku ps
    Process       State               Command
    ------------  ------------------  ------------------------------
    web.1         up for 5s           python hello.py 5000
    

    访问自己的Heroku应用地址,例如我的是http://simple-mountain-9034.herokuapp.com/abcdefg,成功!

    参考资料:


    博客迁移到Linode Tokyo IDC

    之前博客是放在Amazon ec2上,用的是一年免费的micro instance。本来放博客还是足够的,但跑点其他应用cpu就不够了。
    micro instance到今年11月也到期了,刚好看到linode推出tokyo idc的vps。ping值在70左右,比美西那些好很多了。用了几天,觉得缺点是下载美国的东西,速度很少能上1M,但为了中国的访问速度,还凑合吧。

    迁移过程觉得运维水平还是太低,做不到一键迁移。