13.8 使用JSON或YAML文件存储配置

    可以使用JSON或YAML文件完成配置值的存储,这种方式相对容易一些。语法相对友好一些。可以使用YAML表达各种各样的数据。然而,使用JSON的话,对象类的范围较窄一些,可像以下代码这样来定义一个JSON配置文件。

    {
       "table":{
         "dealer":"Hit17",
         "split":"NoResplitAces",
         "decks":6,
         "limit":50,
         "payout":[3,2]
       },
       "player":{
         "play":"SomeStrategy",
         "betting":"Flat",
         "rounds":100,
         "stake":50
       },
       "simulator":{
         "samples":100,
         "outputfile":"p2_c13_simulation.dat"
       }
    }

    JSON文档看起来像是嵌套的字典,这正是在加载文件时所创建的对象,可以使用以下代码来加载一个配置文件。

    import json
    config= json.load( "config.json" )

    这样就可以使用config['table']['dealer']来查找用于庄家规则的类,也可以使用['player']['betting']来查找玩家特定的玩牌策略的类名。

    不像 INI文件,可以像对待一个值序列那样,对tuple进行编码。按照这个逻辑,config['table']['payout']的值可以当作是两个元素的序列,并不必严格遵照tuple那样的处理方式。然而,在访问配置文件时没有必要一定要使用ast.literal_eval()

    以下代码演示了我们如何使用这个嵌套结构,这里只列出main_nested_dict()函数的第1部分。

    def main_nested_dict( config ):
       dealer_nm= config.get('table',{}).get('dealer', 'Hit17')
       dealer_rule= {'Hit17':Hit17(),
         'Stand17':Stand17()}.get(dealer_nm, Hit17())
       split_nm= config.get('table',{}).get('split', 'ReSplit')
       split_rule= {'ReSplit':ReSplit(),
         'NoReSplit':NoReSplit(),
         'NoReSplitAces':NoReSplitAces()}.get(split_nm, ReSplit())
       decks= config.get('table',{}).get('decks', 6)
       limit= config.get('table',{}).get('limit', 100)
      
    payout= config.get('table',{}).get('payout', (3,2))
       table= Table( decks=decks, limit=limit, dealer=dealer_rule,
         split=split_rule, payout=payout )

    这与之前所演示的main_ini()函数类似。如果使用configparser与上一个版本的实现对比一下,就会发现复杂度基本是一样的。命名方面相对简单了一些,使用了get('table',{}).get('decks')来代替config.getint('table','decks')

    最大的不同是加粗显式的那行代码。JSON格式提供了正确解码的整数值和值序列。不必使用eval()ast.literal_eval()来对tuple进行解码。另一部分中,创建PlayerSimulate对象的配置,和main_ini()的版本是类似的。

    13.8.1 使用压平的JSON配置

    如果使用将多个配置文件进行集成的方式来完成默认值的初始化,就不能同时使用ChainMap和嵌套的字典结构。因此,要么对程序的参数压平,要么对不同的参数数据源进行合并。

    可以在名字之间简单地使用.操作符来对命名进行压平操作。下面所示的是一个压平后的JSON文件。

    {
    "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_simulation.dat",
    "simulator.samples": 100
    }

    以上文件的优势是,可以使用ChainMap计算来自不同参数源的配置值。也简化了对指定参数值查找的语法。当拿到一个配置文件名列表时,config_names,可以像如下代码这样操作。

    config = ChainMap( *[json.load(file) for file in reversed(config_names)] )

    以上代码基于一个反序的配置文件名列表创建了ChainMap。为什么要反序?因为所希望的列表顺序为,特殊化的在前面,一般化的在后面。configparser所需要的列表也是反序的,而且通过将新项持续地添加到列表头部以达到持续创建ChainMap的目的,反序也是必需的。在这里,只是简单地加载一个dict列表进入ChainMap中,而且在使用key进行搜索时,第1个dict将出现在搜索结果的第1个位置。

    可以使用一个类似这样的方法来加强ChainMap的实现。以下代码演示了第1部分,创建Table实例。

    def main_cm( 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= config.get('table.payout', (3,2))
       table= Table( decks=decks, limit=limit, dealer=dealer_rule,
         split=split_rule, payout=payout )

    至于其余部分,创建PlayerSimulate对象的配置,和main_ini()版本的实现是类似的。

    使用configparser将这个版本的实现与之前的比较,可以看到复杂度基本是相同的。以上实现在命名上相对简单,使用了int(config.get('table.decks'))来代替config.getint('table','decks')

    13.8.2 加载YAML配置

    因为YAML语法包括了JSON的语法,因此对于之前的示例,可以使用YAML或者JSON加载。以下定义的是一个JSON文件,使用的是嵌套字典的写法。

    player:
     betting: Flat
     play: SomeStrategy
     rounds: 100
     stake: 50
    table:
     dealer: Hit17
     decks: 6
     limit: 50
     payout: [3, 2]
     split: NoResplitAces
    simulator: {outputfile: p2_c13_simulation.dat, samples: 100}

    比起纯JSON,这个文件的语法好一些,更容易进行编辑。对于配置中大多数为字符串和整数的应用,这种格式有很多好处。加载过程和加载JSON文件是相同的。

    import yaml
    config= yaml.load( "config.yaml" )

    和嵌套字典所遇到的限制一样,需要对命名进行压平来解决默认值的问题。

    如果要支持除字符串和整数外的更多类型,可以通过对类名进行编码并创建自定义的实例来扩展YAML。这里是一个YAML文件,用于创建模拟器所需的配置对象。

    # Complete Simulation Settings
    table: !!python/object:main.Table
     dealer: !!python/object:main.Hit17 {}
     decks: 6
     limit: 50
     payout: !!python/tuple [3, 2]
     split: !!python/object:main.NoReSplitAces {}
    player: !!python/object:main.Player
     betting: !!python/object:main.Flat {}
     initstake: 50
     maxrounds: 100
     play: !!python/object:__main
    .SomeStrategy {}
     rounds: 0
     stake: 63.0
    samples: 100
    outputfile: p2_c13_simulation9.dat

    我们对类名和实例的构造器使用 YAML 进行了编码,这样就可以对 TablePlayer定义完整的初始化,可以像以下这样使用初始化文件。

    import yaml
    if name == "main":
       config= yaml.load( yaml1_file )
       simulate( config['table'], config['player'],
         config['outputfile'], config['samples'] )

    在上例中,可以看到YAML配置文件可以直接被编辑。YAML格式具备Python同样的能力,但需要更复杂的语法。对于上例来说,使用Python配置脚本比YAML更好一些。