Python食谱-1.24.使指定的字符串忽略大小写

原文作者:Dale Strickland-Clark, Peter Cogolo, Mark McMahon
中文翻译:Tony (digitalsatori)

问题

如何才能使某些字符串在比较或查询时能忽略大小写,而在其它操作时能保持其原来的大小写?

解决方法

最佳的方案就是把这样需求的字符串封装在一个如下例所示的字符串的子类中:

class iStr(str):
    """
    忽略大小写字符串类。
    除了在比较和搜索时忽略大小写,其它与str的表现完全一样,
    """
    def _ _init_ _(self, *args):
        self._lowered = str.lower(self)
    def _ _repr_ _(self):
        return '%s(%s)' % (type(self)._ _name_ _, str._ _repr_ _(self))
    def _ _hash_ _(self):
        return hash(self._lowered)
    def lower(self):
        return self._lowered
def _make_case_insensitive(name):
    ''' 将str中的一个方法改为忽略大小写后封装到iStr中。'''
    str_meth = getattr(str, name)
    def x(self, other, *args):
        ''' 尝试将'other'转为小写。通常情况下others是字符串,
            但因为字符串可以跟其他非字符串作比较,
            所以可能有无法转为小写的形式而保持其原样。
        '''
        try: other = other.lower( )
        except (TypeError, AttributeError, ValueError): pass
        return str_meth(self._lowered, other, *args)
    # 对于Python2.4 以后的版本,可以添加以下语句: x.func_name = name
    setattr(iStr, name, x)
# 对指定的方法调用  _make_case_insensitive 函数
for name in 'eq lt le gt gt ne cmp contains'.split( ):
    _make_case_insensitive('_ _%s_ _' % name)
for name in 'count endswith find index rfind rindex startswith'.split( ):
    _make_case_insensitive(name)
# 这里注意我们没有修改的方法有 'replace', 'split', 'strip', ...
# 当然如果你原意,完全可以将他们也添加进来
del _make_case_insensitive    # 不再需要这个辅助函数了,删除之

讨论

构建这个iStr类值得我们注意的是:首先,因为考虑到字符串的小写版本会在未来的使用中频繁的使用,我们在 __init__ 方法中一次性创建了其小写版本。该小写版本的字符串保存在类的私有属性中,但不是"强制私有"(即,我们使用一个前置下划线而不是两个)。这是因为如果要创建iStr的子类(比如象解决方案方案中提到的那样创建一个子类扩展iStr目前没有涉及的忽略大小写的字符串分割,替换等功能),该子类可能需要访问到其父类的内部信息。iStr中没有提供诸如replace 等的"忽略大小写"版本的方法是因为这样做在一般情况下会让输入输出的关系变得混淆不清。因此,我们鼓励在特别的应用中,通过创建子类来扩展所需的动能。比方说,因为replace 没有被封装在 iStr 中,对iStr的实例调用replace方法,返回的是str实例,而非iStr实例。如果这会给程序带来问题,我们可以将iStr的返回字符串的方法进行封装,确保其返回的是iStr 的实例。为此你需要创建一个类似于"解决方案"中的辅助函数:

def _make_return_iStr(name):
    str_meth = getattr(str, name)
    def x(*args):
        return iStr(str_meth(*args))
    setattr(iStr, name, x)

然后你要对所有会返回字符串的相关字符串方法调用以上的辅助函数:

for name in 'center ljust rjust strip lstrip rstrip'.split( ):
    _make_return_iStr(name)

字符串中有将近20个此这类方法(包括诸如 __add__, __mul__ 等特殊方法),你还可以对比如 splitjoin 进行类似的封装,但要做一些特殊处理。而对于 encode decode 这样的方法就无法这样封装了,除非我们也设计了忽略大小写的unicode子类。在实际应用中,这些方法都被证实运行正常可靠。通过本例可见Python字符串对象的丰富功能使全局性的自定义工作颇费功夫。

iStr的构筑中我们尽力避免使用繁复的代码(重复而容易出错的代码)来重构字符串中的每个方法。自定义的 metaclass 或者其它的高级技术在本例中也不适用。我们创建了一个辅助函数并以此创建并安装clousre封装函数, 然后通过两个循环来调用该函数,一个循环服务于普通方法,另一个服务于特殊方法。该循环要置于类定义之后,因为它们是用来修改iStr类对象的,必须在创建了类对象之后 才能对其修改。

Python2.4及以上的版本可以为函数对象的 func_name 重新赋值,这样做可以在对 iStr 实施自省操作(比如在互动python解释器中使用help函数)得到一个清晰无误的结果。但是Python2.3将函数对象中的 func_name 属性视为只读,因此在本配方中只是在备注中提到了这种用法,而没有实际的实施以避免对Python2.3不兼容。

在操作中忽略大小写(但是保留原来的大小写形式)的字符串有很多的用途。比如,可以提供更加通融的用户输入,用于诸如windows或Macintosh的忽略大小写的文件系统的的文件名匹配。我们可能需要创建各种各样的"忽略大小写"的容器类,比如字典,列表,集合等等使容器成员或字符串键能忽略大小写。一个比较好的方法是一次性定义好"忽略大小写"的比较和搜索功能。在创建"忽略大小写容器类"时,或改变容器成员时,我们可以利用本配方中的 iStr 将其中的 str 转变为 iStr 实例。

比如,对于一个其成员都为字符串的列表,希望能按照忽略大小写的方式来处理(比如在使用 sort count index 等方法时):

class iList(list):

    def _ _init_ _(self, *args):
        list._ _init_ _(self, *args)
        # rely on _ _setitem_ _ to wrap each item into iStr...
        self[:] = self
    wrap_each_item = iStr
    def _ _setitem_ _(self, i, v):
        if isinstance(i, slice): v = map(self.wrap_each_item, v)
        else: v = self.wrap_each_item(v)
        list._ _setitem_ _(self, i, v)
    def append(self, item):
        list.append(self, self.wrap_each_item(item))
    def extend(self, seq):
        list.extend(self, map(self.wrap_each_item, seq))

如果你希望在特定的程序中对上例使用自定义的 iStr 的子类,只需要继承 iList 并覆写一个类成员 wrapped_each_item

参考

Python库参考手册和Python in a Nutshell 中关于 str, 字符串方法的章节,以及关于用于比较和hashing的特殊方法的说明。

Leave a Response