13.9 使用特性文件存储配置
特性文件通常在Java程序中使用。在Python中一样可以使用它们。它们解析起来更容易,并且可以使用方便的容易掌握的格式来对配置参数进行编码。有关这种格式的更多信息,可参见:http://en.wikipedia.org/wiki/.properties。如下是一个特性文件的示例。
# Example Simulation Setup
player.betting: Flat
player.play: SomeStrategy
player.rounds: 100
player.stake: 50
table.dealer: Hit17
table.decks: 6
table.limit: 50
table.payout: (3,2)
table.split: NoResplitAces
simulator.outputfile = p2_c13_simulation8.dat
simulator.samples = 100
以上文件的语法更简洁,这是它的一个优势。section.property这样的命名很常见。在一个复杂的配置文件中,它的名字会很长。
13.9.1 解析特性文件
在 Python 标准库中,没有内置的特性解析器。可以从 Python 的包管理系统中下载一个特性文件解析器(https://pypi.python.org/pypi)。然而,它不是一个复杂的类,在高级面向对象编程中它是一个不错的实践。
我们将这个类分为最外层的 API 函数和较为底层解析函数。这里是一些全局的 API方法。
import re
class PropertyParser:
def read_string( self, data ):
return self._parse(data)
def read_file( self, file ):
data= file.read()
return self.read_string( data )
def read( self, filename ):
with open(filename) as file:
return self.read_file( file )
以上实现的功能是对文件名、文件或是一个文本块进行解析。它沿用了configparser的设计风格。一种常见的方式(更少的使用方法)是使用isinstance()来决定参数类型以及处理逻辑。
文件名是字符串类型,文件通常是io.TextIOBase的实例。文本块也是字符串,基于这一点,许多库使用load()来加载文件和文件名,使用loads()来加载简单的字符串。以下这种设计方式和json模块是一致的。
def load( self, file_or_name ):
if isinstance(file_or_name, io.TextIOBase):
self.loads(file_or_name.read())
else:
with open(filename) as file:
self.loads(file.read())
def loads( self, string ):
return self._parse(data)
这些方法同样支持文件、文件名或文本块。这些附加的方法名提供了一个API,使用起来更简便。重要的是,在不同的库、包和模块中,需要进行一致性的设计。以下是_parse()方法的实现。
key_element_pat= re.compile(r"(.?)\s(?<!\)[:=\s]\s(.)")
def _parse( self, data ):
logical_lines = (line.strip()
for line in re.sub(r"\\n\s*", "", data).splitlines())
non_empty= (line for line in logical_lines
if len(line) != 0)
non_comment= (line for line in non_empty
if not( line.startswith("#") or line.startswith("!") ) )
for line in non_comment:
ke_match= self.key_element_pat.match(line)
if ke_match:
key, element = ke_match.group(1), ke_match.group(2)
else:
key, element = line, ""
key= self._escape(key)
element= self._escape(element)
yield key, element
这个方法使用了3个表达式生成器,用于对特性文件中的物理行和逻辑行提供一些全局功能。表达式生成器分为3种语法规则。支持延迟执行是它的一个优势。在for line in non_comment语句执行之前,表达式生成器不会产生任何的中间结果。
对于第1个表达式,赋值给了logical_lines变量,对以\为结尾的物理行进行合并,来创建更长的逻辑行。开头(和结尾)的空格被移除了,只留下了行内容。正则表达式r"\n\s*"用于匹配所有以\为结尾的当前行以及所有开头是空格的下一行。
第2个表达式赋值给了non_empty,只会对长度大于0的行进行迭代。它会过滤掉空白行。
第3个表达式,non_comment只会对没有以#或!开头的行进行迭代,以#或!为开头的行会被过滤掉。
使用了这3种表达式生成器,for line in non_comment循环只会对没有注释的、非空的、移除了空格的逻辑行进行迭代。在循环体中,在剩余的行中取出一部分,将键和值分离,然后执行self._escape()函数来对转义序列进行扩展。
键值对的模式key_element_pat会查找没有转义的: , =或者是周围都是空白的空格。它使用断言进行了封装,使用(?<!\)来表示正则表达式必须是没有被转义的,之后的模式都不能以\开头。这意味着(?<!\)[:=\s]是没有被转义的: , 或=,或者空格。
如果键值对模式没有匹配到任何结果,就意味着缺少分隔符。我们将这种情况视为没有提供与相关键匹配的值。
因为键值构成了两个元素的元组,它可以被转换为字典,它提供了一个配置的映射表,就像之前所看到的一些配置表示模型一样。它也可以作为一个序列来展示排序好的原文件的内容。最后的部分是一个方法,用于将转义符转换为最终字符。
def _escape( self, data ):
d1= re.sub( r"\([:#!=\s])", lambda x:x.group(1), data )
d2= re.sub( r"\u([0-9A-Fa-f]+)", lambda x:chr(int(x.
group(1),16)), d1 )
return d2
_escape()方法函数执行了两次替换。第1次将转义的标点符号以纯文本的格式替代,将\:,#,!,\=的\移除。对于Unicode转义符,进制字符串用于创建正确的Unicode字符,替换掉\uxxxx序列。将十六进制的数转换为整数,进行了字符替换。
为了消除中间变量,两次替换可以被合并为一次操作,这样可以提高性能。合并后的代码如下。
d2= re.sub( r"\([:#!=\s])|\u([0-9A-Fa-f]+)",
lambda x:x.group(1) if x.group(1) else chr(int(x.
group(2),16)), data )
而这一小部分的性能优化与复杂的正则表达式以及替换函数所带来的开销互相抵消了。
13.9.2 使用特性文件
在使用特性文件时,有两种选择。可以使用configparser的设计方式对多个文件进行解析,然后基于不同值的合并结果创建一种映射。或者使用ChainMap模式为每个配置文件创建特性序列的映射。
ChainMap处理过程相对简单,并提供了所需的功能。
config= ChainMap(
*[dict( pp.read(file) )
for file in reversed(candidate_list)] )
我们将文件列表顺序翻转了:包含最特殊化设置的放在列表中第1个位置,包含最一般化配置的放最后面。一旦完成了 ChainMap 的加载,就可以使用特性来完成 Player、Table和Simulate实例的创建和初始化。
比起要为一种包含了多个数据源的映射做更新,这种方式似乎更容易一些。它沿用了用于处理JSON和YAML配置文件的方式。
还可以使用这样的方式来对ChainMap进行扩展,与之前看到的main_cm()函数类似。这里是代码的第1部分,创建Table实例。
import ast
def main_cm_str( config ):
dealer_nm= config.get('table.dealer', 'Hit17')
dealer_rule= {'Hit17':Hit17(),
'Stand17':Stand17()}.get(dealer_nm, Hit17())
split_nm= config.get('table.split', 'ReSplit')
split_rule= {'ReSplit':ReSplit(),
'NoReSplit':NoReSplit(),
'NoReSplitAces':NoReSplitAces()}.get(split_nm, ReSplit())
decks= int(config.get('table.decks', 6))
limit= int(config.get('table.limit', 100))
payout= ast.literal_eval(config.get('table.payout', '(3,2)'))
table= Table( decks=decks, limit=limit, dealer=dealer_rule,
split=split_rule, payout=payout )
与 main_cm()函数实现的不同点在于 payout 元组的处理。在之前的实现里,JSON(和YAML)可以解析元组。当使用特性文件时,所有的值都是简单的字符串。必须使用eval()或ast.literal_eval()来执行传入的值。main_cm_str()函数的其余部分与main_cm()函数是相同的。
