1.24 让某些字符串大小写不敏感
任务
你想让某些字符串在比较和查询的时候是大小写不敏感的,但在其他操作中却保持原状。
解决方案
最好的解决方式是,将这种字符串封装在str的一个合适的子类中:
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小写化,通常这应该是一个字符串,
但必须要做好准备应对这个过程中出现的错误,
因为字符串是可以和非字符串正确地比较的
'''
try: other = other.lower( )
except (TypeError, AttributeError, ValueError): pass
return str_meth(self._lowered, other, *args)
# 仅Python 2.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的子类很有可能会需要访问其父类iStr的一些关键的“实现细节”。
这里我们没有提供其他一些方法的大小写不敏感的版本,如replace,因为这个例子已经清晰地展示了一种通用的建立输入和输出之间联系的方式。根据应用进行特别定制的子类将提供最能够满足需求的功能。比如,replace方法并没有被封装,则我们对一个iStr的实例调用replace,返回的是str的实例,而不是iStr。如果这会给你的应用带来问题,可以将所有的返回字符串的iStr方法封装起来,这就可以确保所有返回的结果是iStr的实例。基于这个目的,需要另一个单独的助手函数,相似但不完全等同于解决方案中给出的_make_case_insensitive:
def _make_return_iStr(name):
str_meth = getattr(str, name)
def x(*args):
return iStr(str_meth(*args))
setattr(iStr, name, x)
需要对所有返回字符串的方法的名字应用这个助手函数,_make_return_iStr:
for name in 'center ljust rjust strip lstrip rstrip'.split( ):
_make_return_iStr(name)
字符串有约20种方法(包括一些特殊方法,比如 _add 和 mul _),需要考虑哪些方法应该被封装起来。也可以把一些额外的方法,比如split和join(它们可能需要一些特别的处理)封装起来,或者其他的方法,如encode和decode,对于此类方法,除非定义了一个大小写不敏感的unicode子类型,否则无法处理它们。而实际上,针对一个特定的应用,可能不是所有的未封装的方法都会引起问题。正如你所见的那样,由于Python字符串的方法和功能很丰富,要想用一种通用的不依赖于特定应用的方式,完全彻底地定制出一个子类型,还是要花点功夫的。
iStr的实现很谨慎,主要是为了避免一些重复性的例行公事般的代码(通常是冗长且容易滋生bug的代码),如果我们用普通的方式重载str每一个需要的方法,在类的实现中写上一堆def语句,很有可能就会陷入这种尴尬的境地。使用可自定义的元类或者其他的高级技术对这个例子而言也不会有什么特别的优势,但使用一个辅助函数来生成和安装封装层闭包,就可以轻易地避开问题。然后我们在两个循环中使用该辅助函数,一个循环处理常用的方法,另一个则处理特殊的方法。这两个循环都必须被放置在class语句之后,正如我们在解决方案中给出的代码所示,这是因为这两个循环需要修改iStr类对象,但除非用class语句完成对iStr类的声明,否则那个类对象根本就不存在(因此当然也无法修改)。
在Python 2.4中,可以重新指定函数对象的func_name属性,在本例中,当对iStr实例应用内省机制时,可以用这种方法让代码变得更加清晰和易读。但在Python 2.3中,函数对象的func_name属性是只读的。因此,在本节的讨论中,我们仅仅是指出了另一种可能性,我们不想因为这个小问题失去对Python 2.3的兼容性。
大小写不敏感(但仍保留了大小写信息)的字符串有很多用途,包括提高对用户输入进行解析的宽松度,在文件系统(比如Windows和Macintosh的文件系统)中查找名字包含指定字符的文件,等等。你可能会发现,有很多地方需要“大小写不敏感”的容器类型,比如字典、列表、集合等—它们都需要在某些场合,忽略掉key或者子项的大小写的信息。很明显,一个好的方法是一次性构建出“大小写不敏感”的比较和查询功能;现在你的工具箱中已经增加了本节提供的解决方案,可以对字符串进行任何需要的封装和定制,你甚至还能定制其他一些你希望具备“大小写不敏感”能力的容器类型。
比如,一个所有子项都是字符串的列表,你希望能够进行一些大小写无关的处理(如用count和index进行排序),完全可以基于iStr的实现,轻易地构建一个iList:
class iList(list):
def _ _init_ _(self, *args):
list._ _init_ _(self, *args)
# 依赖_ _setitem_ _将各项封装为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))
本质上,我们做的事情是把iList实例中每个子项都通过调用iStr来封装,其余部分则保持原状。
另外提一句,iList的实现方式使得可以根据应用提供特定的iStr,轻易地完成对子类的定制:只需在iList的子类中重载成员变量wrap_each_item即可。