13.5 使用PY文件存储配置
PY文件格式意味着使用Python代码作为配置文件以及实现应用程序的语言。我们将有一个配置文件,它只是一个简单的模块,配置文件的语法就是Python。这样就不需要解析过程。
使用Python时需要在设计上注意几点。我们有两个全局的策略来使用Python作为配置文件。
- 一个最上层的脚本:在这种情况下,配置文件只是最上层的主程序。
- 一个exec()的导入:在这种情况下,配置文件需要为模块的全局变量提供参数值。
我们可以设计一个最上层脚本文件,如下代码所示。
from simulator import *
def simulateSomeStrategyFlat():
dealerrule= Hit17()
splitrule= 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 )
simulate( table, player, "p2_c13_simulation3.dat", 100 )
if __name == "__main":
simulate_SomeStrategy_Flat()
以上代码意味着,不同的配置参数用于创建和初始化对象。我们只是简单地将配置参数写入代码。我们将逻辑提到了一个单独的函数simulate()中。
使用Python作为配置语言的一个劣势是Python语法潜在的复杂性。基于两点原因,这个问题通常是无关紧要的。首先,如果在设计上仔细考虑,配置的语法应该是由一些简单的赋值语句例如()和,组成。其次,更重要的一点是,其他配置文件有它们自己复杂的语法,与Python的语法区分开了。只使用一种语言可以降低复杂度。
simulate()函数是从全局的simulator应用导入的,这个simulate()函数的实现可能会如下代码所示。
import csv
def simulate( table, player, outputfile, samples ):
simulator= Simulate( table, player, samples )
with open(outputfile,"w",newline="") as results:
wtr= csv.writer( results )
for gamestats in simulator:
wtr.writerow( gamestats )
这个函数与桌子、玩家、文件名和其他例子是通用的。
这种配置技术的难点在于缺少方便的默认值。最上层的脚本必须要完成:所有的配置参数都必须进行设置。为所有参数都赋值可能显得有点麻烦,为什么要为很少使用的参数设置默认值?
在一些情况下,这点没有限制。对于一些默认值很重要的场景,会围绕这个限制介绍两种方案。
13.5.1 使用类定义进行配置
使用最上层脚本配置有时遇到的难点是缺少方便的默认值。为了提供默认值,可以使用普通的类继承。以下是我们如何使用类定义并基于配置值来创建对象的。
import simulation
class Example4( simulation.Default_App ):
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 )
outputfile= "p2_c13_simulation4.dat"
samples= 100
这样的话,就可以使用默认配置来定义Default_App。所定义的类还可以被缩减,只提供Default_App中需要被重写的值。
也可以使用mixin来将定义分为可重用的几个部分。可将我们的类分为桌子、玩家和模拟器组件,然后通过mixin将它们组合在一起。有关mixin类设计的更多信息,可参见第8章“装饰器和mixin——横切方面”。
在两种方式中,类定义的使用到达了瓶颈。没有包含方法定义,只准备使用这个类来定义一个实例。然而,可以对一小部分代码段进一步缩减,这样赋值语句就会只在很小的命名空间中,看起来更简洁。
可以修改simulate()函数,类定义中接收一个参数。
def simulate_c( config ):
simulator= Simulate( config.table, config.player, config.samples )
with open(config.outputfile,"w",newline="") as results:
wtr= csv.writer( results )
for gamestats in simulator:
wtr.writerow( gamestats )
这个函数从全局的配置对象中获取了相关的值,然后用于创建一个Simulate实例并执行。执行结果与之前例子中的simulate()函数相同,但是参数结构不同。以下代码演示了如何为函数传递单一实例的。
if name == "main":
simulation.simulate_c(Example4())
这种方式的一个小缺陷是,它与用于从命令行中获取参数的argparse并不兼容,可以使用types.SimpleNamespace对象来解决这个问题。
13.5.2 通过SimpleNamespace进行配置
我们可以使用types.SimpleNamespace对象来根据需要进行属性的添加,这和类定义是类似的。在定义一个类时,所有的赋值语句都在类中完成。当创建一个SimpleNamespace对象时,需要显式指定每一个需要获取的Namespace的名称。理想情况下,可以使用如下代码创建SimpleNamespace。
>>> import types
>>> config= types.SimpleNamespace(
… param1= "some value",
… param2= 3.14,
… )
>>> config
namespace(param1='some value', param2=3.14)
如果所有的配置项相互独立,那么它会很好地运行。然而,在以上例子中,在配置中存在一些复杂的依赖。可使用以下两种方式中的任意一种来解决。
- 可以只提供依赖项的值,赋值操作交给应用完成。
- 可以在递增的命名空间中赋值。
只创建依赖项的值的代码如下。
import types
config5a= types.SimpleNamespace(
dealer_rule= Hit17(),
split_rule= NoReSplitAces(),
player_rule= SomeStrategy(),
betting_rule= Flat(),
outputfile= "p2_c13_simulation5a.dat",
samples= 100,
)
config5a.table= Table( decks=6, limit=50, dealer=config5a.dealer_rule,
split=config5a.split_rule, payout=(3,2) )
config5a.player= Player( play=config5a.player_rule, betting=config5a.
betting_rule,
rounds=100, stake=50 )
在这里,使用了6个互相独立的值为配置创建了SimpleNamespace。然后,对配置进行了修改,添加了两项,它们依赖于其中的4个配置项。
config5a对象在对象中几乎是唯一的,它的创建是由之前的例子中的Example4()方法完成的。基类是不同的,属性集合以及它们的值是唯一的。下面是另一种方案,在最上层的脚本中递增地创建配置。
import types
config5= types.SimpleNamespace()
config5.dealer_rule= Hit17()
config5.split_rule= NoReSplitAces()
config5.table= Table( decks=6, limit=50, dealer=config5.dealer_rule,
split=config5.split_rule, payout=(3,2) )
config5.player_rule= SomeStrategy()
config5.betting_rule= Flat()
config5.player= Player( play=config5.player_rule, betting=config5.
betting_rule,
rounds=100, stake=50 )
config5.outputfile= "p2_c13_simulation5.dat"
config5.samples= 100
同样的,在这一类的配置上,可使用之前所示的simulate_c()方法。
糟糕的是,会遇到在使用最上层脚本进行配置时所遇到的同样问题。没有简便的方法来为配置对象提供默认值,或许期望使用一个工厂函数来执行导入操作,其中包括使用适当的默认值来创建SimpleNamespace。
From simulation import make_config
config5= make_config()
以上代码执行后,就可从工厂函数make_config()中获得已赋值的默认值。每一个用户指定的配置只需提供需要重写的默认值。
make_config()函数的默认实现代码可能如下。
def make_config( ):
config= types.SimpleNamespace()
# set the default values
config.some_option = default_value
return config
在make_config()函数中,使用赋值语句对默认配置进行设置。然后应用只需对需要重写的值进行设定。
config= make_config()
config.some_option = another_value
simulate_c( config )
这样一来,就可以在应用中创建配置并使用一种相对简单的方式来使用它。脚本的核心逻辑很精简。如果使用关键字参数,可以使实现更灵活。
def make_config( **kw ):
config= types.SimpleNamespace()
# set the default values
config.some_option = kw.get("some_option", default_value)
return config
这样我们就可以创建如下代码所示的配置。
config= make_config( some_option= another_value )
simulate_c( config )
以上的实现更简洁,并且和之前例子的逻辑一样清晰。
所有关于第 1 章“init()方法”中介绍的技术,都应用在这类工厂函数配置的定义中了。如果需要,可以使它的实现具有很高灵活性。它的优势是可以和解析命令行的argparse模块很好地结合。我们会在第16章“使用命令行”中对这部分进行展开。
13.5.3 在配置中使用Python的exec()
当决定使用Python作为配置格式时,可以在一个指定的命名空间中使用exec()函数来执行一段代码。假设写了一个如下所示的配置文件。
# SomeStrategy setup
# Table
dealer_rule= Hit17()
split_rule= NoReSplitAces()
table= Table( decks=6, limit=50, dealer=dealer_rule,
split=split_rule, payout=(3,2) )
# Player
player_rule= SomeStrategy()
betting_rule= Flat()
player= Player( play=player_rule, betting=betting_rule,
rounds=100, stake=50 )
# Simulation
outputfile= "p2_c13_simulation6.dat"
samples= 100
以上所示的是一个可读性很好的配置参数集合。它与接下来的节中要介绍的INI文件和特性文件类似,可以执行这个文件,使用exec()函数创建一种命名空间。
with open("config.py") as py_file:
code= compile(py_file.read(), 'config.py', 'exec')
config= {}
exec( code, globals(), config )
simulate( config['table'], config['player'],
config['outputfile'], config['samples'])
在上例中,显式地使用compile()函数创建了一个对象。这是不必要的,可以只是简单地将文件的文本传入exec()函数,它可以直接执行代码。
exec()函数提供了3个参数:代码、存放全局变量的字典以及用于存放将要被创建的本地变量的字典。代码块执行后,随着赋值语句的执行,创建了本地变量字典。在上例中,是config变量,键将作为变量名。
接下来在程序初始化过程中,可以使用它来创建对象。为simulate()函数传入所需的对象来执行模拟过程。config变量将获得本地变量值,如下代码所示。
{'bettingrule': <main.Flat object at 0x101828510>,
'dealerrule': <main.Hit17 object at 0x101828410>,
'outputfile': 'p2c13simulation6.dat',
'player': <main.Player object at 0x101828550>,
'playerrule': <_main.SomeStrategy object at 0x1018284d0>,
'samples': 100,
'split_rule': <__main.NoReSplitAces object at 0x101828450>,
'table': <__main.Table object at 0x101828490>}
然而,初始化必须是一个可写的字典格式:config['table'],config['player']。
由于字典格式不方便,因此可使用一种设计模式,基于第3章“属性访问、特性和修饰符”所介绍的技术来实现。下面是一个类,基于字典的键返回了命名的特性。
class AttrDict( dict ):
def getattr( self, name ):
return self.get(name,None)
def setattr( self, name, value ):
self[name]= value
def dir( self ):
return list(self.keys())
在使用这个类时,仅当键和Python变量名匹配时才可以。有趣的是,如果以如下代码所示的方式来初始化config变量,那么这一切都可以使用exec()函数来创建。
config= AttrDict()
然后,可以使用一种简单的属性格式(config.table、config.player)来实现对象的创建和初始化。这一小部分语法糖在复杂的应用中是有帮助的。其中一种方式是定义这样一个类。
class Configuration:
def init( self, **kw ):
self.dict.update(kw)
然后就可以使用命名的属性来将简单的dict转换为对象。
config= Configuration( **config )
以上代码使用了简洁的属性名的方式来将dict转换为对象。当然,仅当字典的键与Python变量名匹配时代码才有效。而且,对于平面结构,它的使用是有限制的。不支持嵌套的字典结构,而这种结构在其他格式中是支持的。
