Category: Python


Python os.walk() 函数中的陷阱

用 VC 写程序多了会留下很多中间文件和辅助文件,浪费不少空间,一个个去清理项目又费时费力,写个脚本吧,以后也好用。
 
然后想当然的写下了这样的语句:
for root, dirs, files in os.walk(path):
    for name in files:  
        if splitext(name)[1] == ‘.sln’:
            print "Cleaning",join(root,splitext(name)[0]),"…"
            dirs = []
            CleanSolution(root,splitext(name)[0])
 
在某文件夹下发现 ‘.sln’ 项目文件则不需再往下层文件夹寻找。根据 os.walk() 函数的解释,在循环中修改 dirs 变量即可控制下层递归访问的文件夹,语句 dirs = [] 把 dirs 置空,则递归访问应到此结束。但是在跟踪执行中发现,虽然 dirs 确实被置空了,os.walk() 的递归访问却并没有终止,仍然依次访问了下层的所有文件夹…为什么呢?
尝试着把 dirs = [] 改为 for dir in dirs: dirs.remove(dir) 后运行得到了理想的结果
但为何 remove() 函数可以,赋值却不行?猛然间想到以前实验的一个例子:
 
例一:
>>> a = [1,2,3]
>>> b = a
>>> b.remove(2)
>>> a
[1, 3]
 
此例很能说明问题,虽然在 b 上执行了 remove(2) 操作,但 a 中的元素也被改动了,说明 b = a 实际上并没有复制 list 中的元素,而只是相当于把一个指针传给了 b,a、b实际都指向同一个 list
 
例二:
>>> a = [1,2,3]
>>> b = a
>>> b = []
>>> a
[1, 2, 3]
 
有了上例的基础再看这个应当很好理解了,b = [] 只相当于把一个空 list 的指针给了 b,a 中的元素自然不受影响
 
例三:
>>> a = [1,2,3]
>>> b = a
>>> b[:] = []
>>> a
[]
 
只是把 b 变成了 b[:],这次 a 中的元素被全部清空
 
最后再次回到一开始的 os.walk() 上,递归中 dirs 实际上是以类似 b = 内部 list 的形式得到了所有子文件夹列表,dirs = [] 就不会有任何结果了。把程序改为
dirs[:] = []
del dirs[:]
即可正确实现。
 
看来还是实践比较有锻炼效果,以前专门注意过 list 的赋值动作,结果这次藏在 os.walk() 函数中依然没有发现,造成了潜在错误…
 
PS. 脚本挺好用,居然腾出了 1GB 多空间
Advertisements

Google App Engine

今天在 cnBeta 上看到 Google App Engine 更新的消息,随便看了下介绍却吓了一跳。这不就一个Web程序开发平台么,免费服务器,太爽了 ^_^
老早就想找提供这类服务的网站,可惜免费的几乎没有。原来四月就已经出来了,哎怎么早没发现呢。。。亏了
每用户500MB空间,如果只用来放程序几乎就用不完了。
目前虽然只支持 Python 语言,不过正合我意。
已经想了N个东东都可以做了,可惜最近没什么时间。哈哈寒假可要好好玩一把

Python decorator 学习

Python 程序一向简洁易读。不过也有例外,这两天 decorator 的语法就让我晕了一阵。不带参数时还好些,带参数的 decorator 定义要三层函数嵌套,网上的例子也比较少,自己想的时候是吃了点苦头。不过真正搞明白了也发现就那么回事。恩,记下此例以备忘:
 
目标描述:
写一个 decorator,使函数调用时能够打印被调用的次数和传给他的参数;且这两个功能由传给 decorator 的两个参数作为开关。
 
第一步:为目标函数func做第一层封装。
def newfunc(*kwds,**dic):
    #do something before the call of func
    if dbg_count:
        cnt[0] += 1
        print "Function %s() calling count= %s" % (func.__name__, cnt[0])
    if dbg_arg:
        print "Args:\t",kwds,dic
    val = func(*kwds,**dic)
    #do something after the call of func
    #log.write( str(val) + os.linesep )
    return val

*kwds 和 **dic 是要传给函数 func 的参数,我们在 newfunc 中把他们截获,做一点准备工作后再调用 func 。dbg_count 和 dbg_arg 是 decorator 接收的两个参数,分别作为是否打印对应信息的开关。cnt 是只有一个元素的 list , 用它的 0 号元素储存函数的调用次数,后面再具体分析。
 
第二步:为 newfunc 做一层封装,将他作为替代 func 的函数返回。
def loadf(func):
    cnt = [0]
    def newfunc(*kwds,**dic):
        #do something before the call of func
        if dbg_count:
            cnt[0] += 1
            print "Function %s() calling count= %s" % (func.__name__, cnt[0])
        if dbg_arg:
            print "Args:\t",kwds,dic
        val = func(*kwds,**dic)
        #do something after the call of func
        #log.write( str(val) + os.linesep )
        return val
    return newfunc

与第一步相比只增加了三行,完成两件事:(1)返回刚刚定义的 newfunc 函数,作为 func 的替代;(2)初始化函数调用计数器 cnt 。第一件事比较好理解,返回 newfunc 函数后,如果不考虑那两个控制参数,loadf 已经可以当成一个不含参数的 decorator 来使用了。关于第二件事,这里实际上 cnt[0] 是作为函数 newfunc 的一个静态变量来使用的。作为 c/c++ 程序员,当遇到给函数添加调用计数器时自然想到用静态变量来储存,但在 Python 中没有明确的 静态变量 声明语法,该如何保存上次函数调用后的一些值呢——嵌套函数,这是 Python 中的方法 (或许有人会说yield 或 Generator,那个虽然我也试过,但用作静态变量时很不自然,暂不讨论)。外层函数中定义的 list或dict 变量,在内层函数中一直有效且多次调用内层函数时不会重复初始化,完全就是静态变量的翻版嘛…这里还有个疑问,既然只用保存一个量,为何不用一个 int 型的 cnt ,而要麻烦的动用 list 呢。嘛,懒得写了,试过你就知道了。
 
第三步:最后一层封装,用来接收那两个控制参数。下面是最终版

def dbg_config(dbg_count=True, dbg_arg=True):
    def loadf(func):
        cnt = [0]
        def newfunc(*kwds,**dic):
            #do something before the call of real function
            if dbg_count:
                cnt[0] += 1
                print "Function %s() calling count= %s" % (func.__name__, cnt[0])
            if dbg_arg:
                print "Args:\t",kwds,dic
            val = func(*kwds,**dic)
            #do something after the call of real function
            #log.write( str(val) + os.linesep )
            return val
        return newfunc
    return loadf

做的事情很简单,接收参数,最后返回 loadf 。来分析一下如何起作用的:
@dbg_config(True,True)
def func(a,b):pass
 
调用 func(1,2) 时,展开后就是
dbg_config(True,True)(func)(1,2)
由于 dbg_config(True,True) 返回的是 loadf ,于是上面继续展开,得到
loadf(func)(1,2)
loadf(func) 又返回 newfunc ,变成
newfunc(1,2)
就把对 func 的调用,通过两层封装,变成了对 newfunc 的调用。newfunc 做点补充工作,最后在内部调用了 func
 
上面,由于 decorator 要接收参数,所以得三层封装。若不带参数时只用两层即可。
 
最后看下使用情况:
>>> @dbg_config(dbg_arg=False)
def foo():pass

>>> foo()
Function foo() calling count= 1
>>> foo()
Function foo() calling count= 2
>>> foo()
Function foo() calling count= 3
通过将 dbg_arg 赋值为 False,关闭打印参数的功能
 
>>> @dbg_config()
def add(a,b,c):
…     return a+b+c

>>> print add(1,2,3)
Function add() calling count= 1
Args: (1, 2, 3) {}
6

>>> print add(3,c=2,b=5)
Function add() calling count= 2
Args: (3,) {’c’: 2, ‘b’: 5}
10
两个功能都打开(默认),打印了调用次数,也打印了参数,最后输出运行结果。
 
应该都比较清楚了。写 decorator 的时候有个 functools 模块好像有工具可以用;另外 yield 从 2.4 版开始可以作为表达式而不是一个语句来使用,灵活性大增,可以搞些复杂的用法了,以后有空再来分析吧。
目前进度:
《A byte of Python》:Finished
《Dive Into Python》:Finished
《Core Python Programming》:half
 
以前只注意语法,没太注意实际代码该如何写。今天想了一下,仅仅一个打开文件操作的异常捕获都要好好思考下该如何布局。
《Core Python Programming》中给出了下面两种选择:

try:
    try:

        ccfile = open(‘carddata.txt’, ‘r’)
        txns = ccfile.readlines()
    except IOError:
        log.write(‘no txns this month\n’)
finally:
    ccfile.close()

或者:

try:
    try:

        ccfile = open(‘carddata.txt’, ‘r’)
        txns = ccfile.readlines()
    finally:
        ccfile.close()
except IOError:
    log.write(‘no txns this month\n’)

不知是作者的疏忽,还是Win平台下的解释器与其他平台有所不同,至少在我的试验中,上面两段代码均有明显问题:
open函数打开文件失败时会抛出 IOError 异常,此时对 ccfile 的赋值语句并没有执行完,ccfile 还未赋值,但代码却转入了异常处理块,并按照 finally 语句块执行 ccfile.close() 操作。此时 ccfile 实际上处于未定义状态,调用未定义标识符又会产生 NameError 异常。
实际上,对文件的打开和读写部分要分开处理异常:若文件未成功打开,则只需进行普通处理,不必进行close()操作。看看官方文档给的例子:

f = open("hello.txt")
try:
    for line in f:
        print line
finally:
    f.close()

open函数并没有和下面的读写部分放在同一个 try 语句块中,这样 finally 的 close 执行时就不会出问题。但他没有处理 open 函数有可能抛出的 IOError 异常。下面放上我认为真正安全的代码:(包括一些试验说明代码,所以看起来有点多)

class TestException(Exception):pass
import os,sys
 
try:
    f = open(‘readfile.txt’)
    try:
        for line in f:
            print line,
        raise TestException
    except IOError:
        sys.stderr.write("can’t read file"+os.linesep)
    except TestException:
        sys.stderr.write("TestException occurred in inner_suite"+os.linesep)
    finally:
        f.close()
        raise TestException    # finally 语句块也可能会引发异常
except IOError,e:
    sys.stderr.write("can’t open file:"+str(e)+os.linesep)
# except SomeError:     此处可继续处理上面 finally_suite 抛出的异常
except TestException:
    sys.stderr.write("TestException occurred in finally_suite"+os.linesep)

假定每块均可能抛出 IOError 或 TestException ,则按上述写法可正确处理两种异常。
以上代码在 Python 2.5 下验证通过。
补充一句:以上 try – except -except -finally 同时出现在一组语句中,从 python 2.5 版开始已经支持这种写法了。有些老版的教程还要分开写……
另外2.6版开始好像又增加了 with 语句专门处理这个问题,现在还没仔细看,以后再说吧。

for 语句 in Python

刚开始学 Python 语法的时候觉得这个 for 比起 C++ 里的 for循环 差远了,到处都那么死板。只能在 list 里挨个访问,要是想从 1 加到 100 岂不是得先用 range() 建一个从 1 到 100 的list了?而且步长改成浮点数竟然都不行……
其实想想看本来这类解释型语言强项就不在这个方面吧。。。比优化、比运行效率的话和编译型的 C/C++ 根本不是一个量级的。
 
但在用过一段之后发现 Python 的开发效率确实不是盖的,那个比较奇怪的 for 语法在其中却起到了关键作用。下面这个是随便写的一个统计当前文件夹及其所有子文件夹空间占用的程序:
 
import os
from os.path import getsize,join
 
outfile = open(‘size.txt’,’w’)
outfile.write("%.2f KB" % (sum( (sum(getsize(join(root, name)) for name in files)) \
    for root, dirs, files in os.walk(‘.’) ) / 1024.0))
outfile.close()
 
可以看到关键部分只有一行语句(虽然有点长)就搞定了,同时完成了文件夹的递归访问、循环统计所有文件大小、并输出到文件……
就这么一句话改成 C++ 的话,函数的递归调用、循环文件夹中的每个文件、文件输出,恐怕也得写个几十行了
 
虽然正常来说 python 大概不会提倡上面那种的写法(貌似已经严重破坏了程序的可读性了,本来python在这方面是引以为豪的),但它确实很能说明问题