Python食谱-1.15.制表符(Tab)的扩展和收缩

原文作者:Alex Martelli, David Ascher
中文编译:Tony (digitalsatori)

问题

如何将字符串中的制表符(Tab)转换为一定数量的空格,或者反之。

解决方法

将制表符转换为几个空格是经常会碰到的工作,我们可以用Python字符串对象的 expandtabs 方法来轻松完成。因为字符串是‘不可变的’(immutable),这个方法实际上返回一个新的字符串,该字符串是原字符串的修改副本。我们可以将与原字符串绑定的变量重新绑定到新得到的字符串上:

mystring = mystring.expandtabs()

以上代码并不改变mystring原本所指定的字符串,而是将mystring重新绑定到了一个 expandtabs 方法返回的新字符串,该新字符串扩展了所有原字符串中的制表符为指定数量的空格。默认情况下,制表符的宽度为8; 你也可以传递一个整数参数给expandtabs来自定义制表符的宽度。

将空格转变为tabs的情况是比较少见的,通常这种情况被称为,Tab的收缩。Python并没有提供一个内置的 unexpand 来处理将空格转换为Tab的任务。但是,我们可以自己写一个函数来完成该任务。对于字符串处理,采用‘分割-处理-再连接‘的方法远比在整个字符串上重复的做转换操作来得快捷:

def unexpand(astring, tablen=8):
    import re
    # split into alternating space and non-space sequences
    pieces = re.split(r'( +)', astring.expandtabs(tablen))
    # keep track of the total length of the string so far
    lensofar = 0
    for i, piece in enumerate(pieces):
        thislen = len(piece)
        lensofar += thislen
        if piece.isspace( ):
            # change each space sequences into tabs+spaces
            numblanks = lensofar % tablen
            numtabs = (thislen-numblanks+tablen-1)/tablen
            pieces[i] = '\t' * numtabs + ' ' * numblanks
    return ''.join(pieces)

上面的 unexpand 函数只能处理单行的字符串;要处理多行的字符串可以使用 ''.join([ unexpand(s) for s in astring.splitlines(True) ]) 注: 上例中numblanks和numtabs的计算方法请参见: 字符串方法expandtabs和制表位(tabstops)

讨论

正则表达式在Python语言中对于字符串的处理并非无可替代,但是有时候使用它们处理某些问题会非常方便。比如在上述的 unexpand 函数中,我们使用了正则表达式的 re.split 方法,其拥有字符串对象的 split 方法所不具有的一个特性: 当正则表达式中包含一个(括号括住的)组, re.split 会返回一个分割片段与‘分割符’(spliter)交互出现的列表。因此在本例中,我们得到一个‘非空格片段’和‘空格片段‘交替的列表。for循环跟踪已处理字符串的长度,将空格字符串转变为对应长度的制表符,并添加相应的空格以保持字符串总的长度。

某些仍然被称为扩展tabs的任务就不象直接调用expandtabs方法那么简单了。这类任务中经常会碰到的是:修正混合使用了tab和空格来缩进的Python源文件,使其变成只用空格缩进的源码。这个任务其实暗含了不少复杂情况,比如,你需要猜测源文件中tab的宽度,并将它们转换为以四个空格为一个缩进的表示;你需要保留包含在字符串中的tabs, 或者, 可能你还需要对docstrings做特殊的处理等等。以下是一个简化的要求,只处理每行起始处的tab,其他地方的tab保留:

def expand_at_linestart(P, tablen=8):
    import re
    def exp(mo):
        return mo.group( ).expand(tablen)
    return ''.join([ re.sub(r'^\s+', exp, s) for s in P.splitlines(True) ])

expand_at_linestart 函数使用 re.sub 函数在字符串中按正则表达式搜索,当发现匹配时调用一个函数,同时将匹配的对象作为参数传递给它,从而获取一个匹配对象的替代字串。为了方便起见,该函数设计成能处理多行字符串,其使用了 splitlines 方法和list comprehension (该用法请参见 改变多行字符串的缩进) 当然其对单行字符串也适用。

如果对tab的处理要求比较复杂,比如要区分处理在字符串中还是在字符串外的tab,或者要区分处理docstrings。其基本原理是进行符号化操作(tokenization)。此外你还要对整个源文件进行解析处理,而不是简单运用字符串和正则表达式操作。如果要做这样的复杂处理,你将会面对大量的工作。在本书第16章可以找到一些帮助你起步的方法。

如果发现处理这类任务是这样的伤脑筋,那么你就会在未来的编程中更加注意使自己的源代码保持Python的推荐风格:只使用空格,4个空格表示一个缩进, 不使用tab,在字符串中使用 \t 而非实际的tabs。使你所使用的编辑器遵守这些约定,这样就能防止这类问题的发生,而不是在发生了这类问题后再费心解决。

参见

在Python的库函数参考的“序列类型"章节中关于字符串方法 expandtabs 的介绍;Perl Cookbook recipe 1.7;Python库函数参考和Python in a Nutshell中关于re模块的介绍。

Leave a Response