13.2 表示、持久化、状态和可用性
当打开一个配置文件时,看到的是人性化的对象或对象集合的状态。当对配置文件进行编辑时,我们改变的是对象的持久化状态,这个对象在应用启动(或重启时)将被重新加载。我们从两种常见的角度来看配置文件。
- 一个或一组从参数名到值的映射。
- 一个序列化了的对象,不仅仅是一个简单的映射。
当试图将配置文件缩减为映射时,我们或许想要对配置文件中的关系范围进行限制。在简单的映射中,一切都要使用名称来引用,而且我们必须克服在第10章“用Shelve保存和获取对象”以及第11章“用SQLite保存和获取对象”中所介绍的键的设计问题(当讨论到shelve和sqlite的键时)。我们为配置中的一部分提供了一个唯一的名称,这样部分之间可以互相引用。
以日志信息为例来看一下它是如何完成对复杂系统进行配置是有帮助的。在 Python 的日志对象中的关系——loggers、formatters、filters和handlers——在创建日志时必须同时使用。在标准库的第16.8节中,介绍了日志配置文件的两种不同语法。我们将在第14章“Logging和Warning模块”中进行介绍。
在一些情况下,对复杂的Python对象进行序列化或直接使用Python代码作为配置文件会更简单一些。如果一个配置文件引入了太多的复杂度,它并没有多少实际价值。
13.2.1 应用程序配置的设计模式
有关应用程序配置有两种核心的设计模式。
- 全局属性映射:使用一个全局的对象,它将包含所有的配置参数。它可以是name: value的映射对或是包含了属性值的对象。它将使用单例设计模式来确保只有一个实例。
- 对象创建:不再使用单例,而是定义了工厂或是工厂的集合,基于配置数据来创建应用程序的对象。这样一来,配置信息只在程序启动时被使用一次。配置信息不会保存在全局的对象中。
全局属性映射的设计非常流行,因为它很简单并且容易扩展。例如我们会用如下代码定义简单对象。
class Configuration:
some_attribute= "default_value"
我们可以使用之前的类定义作为一个全局的属性容器。在初始化时,需要完成部分配置文件的解析逻辑,如下代码所示。
Configuration.some_attribute= "user-supplied value"
在程序中的其他部分,就可以使用Configuration.some_attribute的值。有一点关于它的改进是使用正式的单例设计模式。这通常由一个全局模块来完成,因为导入会相对简单,使用了一种全局定义的访问方式。
可能会有一个模块名称为configuration.py。在这个文件中,可能会有如下的定义。
settings= dict()
现在就可以将configuration.settings看作是应用设置的一个全局的库。函数或类可以解析这个配置文件,使用应用将使用的配置值来加载这个字典。
在21点游戏的模拟中,可能会看到如下代码。
shoe= Deck( configuration.settings['decks'] )
或者,可能会看到如下的这段代码。
If bet > configuration.settings['limit']: raise InvalidBet()
通常情况下,我们避免使用全局变量。因为全局变量在程序的任何部分都是可见的,它可能会被忽视,可以使用对象的构造替代全局变量来更好地对配置进行处理。
13.2.2 使用对象的构造完成配置
当使用对象的构造配置应用程序时,目的是创建所需的对象。配置文件需要为所需创建的对象提供不同的初始化参数。
通常可以在单一的、全局的main()函数中将对象构建的初始化过程的逻辑进行集中处理,它将创建在应用程序中使用的对象。我们将在第16章“使用命令行”中重温这一部分,并对这些设计问题进行展开介绍。
现在考虑对21点游戏中的打牌策略进行模拟。当运行模拟器时,希望统计指定的自变量组合的性能情况。这些变量可能包含一些牌场制度,其中包括牌副的数量、桌的限制和庄家规则。变量可能包括玩家的游戏策略,用于叫牌、停叫、分牌和双倍,也包括玩家的玩牌策略、平均投注、鞅投注或一些更特殊的投注系统。一开始代码可能会像如下这样。
import csv
def simulate_blackjack():
dealer_rule= Hit17()
split_rule= NoReSplitAces()
table= Table( decks=6, limit=50, dealer=dealer_rule,
split=split_rule, payout=(3,2) )
player_rule= SomeStrategy()
betting_rule= Flat()
player= Player( play=player_rule, betting=betting_rule,
rounds=100, stake=50 )
simulator= Simulate( table, player, 100 )
with open("p2_c13_simulation.dat","w",newline="") as results:
wtr= csv.writer( results )
for gamestats in simulator:
wtr.writerow( gamestats )
这只是一种快速的实现版本,对所有的对象和初始化值进行了硬编码。我们需要为对象和它们的初始化值添加配置参数。
Simulate类中会有一个API,代码如下。
class Simulate:
def init( self, table, player, samples ):
"""Define table, player and number of samples."""
self.table= table
self.player= player
self.samples= samples
def iter( self ):
"""Yield statistical samples."""
这样一来,我们就可以使用一些初始化参数来创建Simulate()对象。一旦创建了一个Simulate()实例,就可以对其迭代来获取一系列对象统计的信息。
有趣的部分是使用配置参数,而非类名。例如,需要一些参数来标识是否为dealer_rule的值创建Hit17或Stand17的实例。类似地,split_rule的值可用于不同的类中,用于表示牌场中不同的分牌规则。
对于其他情形,参数被用来为类中的init()方法提供参数。例如,牌副的数量、下注限制和21点的账单都将作为配置项用于创建Table实例。
一旦创建了对象,对象就可以使用Simulate.run()方法来生成静态的输出。不再需要一个全局的参数池:参数值会通过它们的实例变量绑定在对象中。
对象创建的设计不像全局特性映射那样简单。它的好处是,避免了使用全局变量,使得参数的处理逻辑集中化并突出了一些主要的工厂函数的使用。
在使用对象构建模式时,添加新参数可能会需要对应用程序进行重构,将参数或关系暴露出来。比起从名称到值的全局映射,它的处理过程显得更复杂。
一个明显的优势是从应用程序中移除了复杂的if语句。在使用策略设计模式时,更倾向于在对象创建时才做决定。此外,简化了逻辑,if语句的消除在性能上也会有所提升。
13.2.3 实现具有层次结构的配置
在选择配置文件的位置时,会有多种方案。以下有5种常见的选择,我们可以使用它们来为配置参数创建具有继承层次结构的配置。
- 应用的安装路径:实际上,它们和基类的定义类似。这里有两种子方案。小的应用可以安装在 Python 的库结构中,初始化文件也可以安装在那里。对于更大的应用,通常会使用自定义名称来对一个或多个安装目录树进行命名。
- Python安装目录:可以使用模块中的
__file__属性来查找一个模块的安装位置。在这里,可以使用os.path.split()来查找配置文件。
- Python安装目录:可以使用模块中的
>>> import this
>>> this.file
'/Library/Frameworks/Python.framework/Versions/3.3/lib/
python3.3/this.py'
- 应用程序安装目录:它将基于自己的名称,因此可以使用
~theapp/和os.path. expanduser()来追踪配置的默认值。
一个系统级的配置目录:它通常会出现在/etc。在Windows中,它会被转化为C:\etc。可以选择包含os.environ['WINDIR']或os.environ['ALLUSERSPROFILE']的值。
当前用户的home目录:一般地,可以使用os.path.expanduser()来将~/转换为用户的home目录。在Windows中,Python会自动使用%HOMEDRIVE%和%HOMEPATH%环境变量。
当前工作目录:通常这个目录为./,尽管os.path.curdir更灵活些。
命令行参数中命名的文件:这是一个显式命名的文件,不需要对命名额外地处理。
一个应用程序可以同时包括以上所有的配置选项,从基类(以上列表的第1个)到命令行参数。使用这种方式,安装默认值是通用的并且很少包含用户的特殊设置,这些值可以使用用户指定的值来重新指定。
意味着通常会有如下代码所示的一个文件列表。
import os
configname= "someapp.config"
configlocations = (
os.path.expanduser("~thisapp/"), # or thisapp.__file,
"/etc",
os.path.expanduser("~/"),
os.path.curdir,
)
candidates = ( os.path.join(dir,config_name)
for dir in config_locations )
config_names = [ name for name in candidates if os.path.exists(name) ]
在以上代码中,得到了一组文件目录并通过将目录与配置文件名进行连接,创建了一个文件名列表。
一旦拿到了配置文件名列表,就可以使用命令行参数将文件名附加到列表的最后面,如下面代码所示。
config_names.append(command_line_option)
它将返回一个位置列表,可用于配置文件或配置默认值的查找。
